##// END OF EJS Templates
store: recommend using `hg debugrebuildfncache` is fncache is corrupted...
Pulkit Goyal -
r42156:3e7cfa17 default
parent child Browse files
Show More
@@ -1,652 +1,654
1 # store.py - repository store handling for Mercurial
1 # store.py - repository store handling for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import functools
11 import functools
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import stat
14 import stat
15
15
16 from .i18n import _
16 from .i18n import _
17 from . import (
17 from . import (
18 error,
18 error,
19 node,
19 node,
20 policy,
20 policy,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 vfs as vfsmod,
23 vfs as vfsmod,
24 )
24 )
25
25
26 parsers = policy.importmod(r'parsers')
26 parsers = policy.importmod(r'parsers')
27 # how much bytes should be read from fncache in one read
27 # how much bytes should be read from fncache in one read
28 # It is done to prevent loading large fncache files into memory
28 # It is done to prevent loading large fncache files into memory
29 fncache_chunksize = 10 ** 6
29 fncache_chunksize = 10 ** 6
30
30
31 def _matchtrackedpath(path, matcher):
31 def _matchtrackedpath(path, matcher):
32 """parses a fncache entry and returns whether the entry is tracking a path
32 """parses a fncache entry and returns whether the entry is tracking a path
33 matched by matcher or not.
33 matched by matcher or not.
34
34
35 If matcher is None, returns True"""
35 If matcher is None, returns True"""
36
36
37 if matcher is None:
37 if matcher is None:
38 return True
38 return True
39 path = decodedir(path)
39 path = decodedir(path)
40 if path.startswith('data/'):
40 if path.startswith('data/'):
41 return matcher(path[len('data/'):-len('.i')])
41 return matcher(path[len('data/'):-len('.i')])
42 elif path.startswith('meta/'):
42 elif path.startswith('meta/'):
43 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')] or '.')
43 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')] or '.')
44
44
45 raise error.ProgrammingError("cannot decode path %s" % path)
45 raise error.ProgrammingError("cannot decode path %s" % path)
46
46
47 # This avoids a collision between a file named foo and a dir named
47 # This avoids a collision between a file named foo and a dir named
48 # foo.i or foo.d
48 # foo.i or foo.d
49 def _encodedir(path):
49 def _encodedir(path):
50 '''
50 '''
51 >>> _encodedir(b'data/foo.i')
51 >>> _encodedir(b'data/foo.i')
52 'data/foo.i'
52 'data/foo.i'
53 >>> _encodedir(b'data/foo.i/bla.i')
53 >>> _encodedir(b'data/foo.i/bla.i')
54 'data/foo.i.hg/bla.i'
54 'data/foo.i.hg/bla.i'
55 >>> _encodedir(b'data/foo.i.hg/bla.i')
55 >>> _encodedir(b'data/foo.i.hg/bla.i')
56 'data/foo.i.hg.hg/bla.i'
56 'data/foo.i.hg.hg/bla.i'
57 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
57 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
58 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
58 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
59 '''
59 '''
60 return (path
60 return (path
61 .replace(".hg/", ".hg.hg/")
61 .replace(".hg/", ".hg.hg/")
62 .replace(".i/", ".i.hg/")
62 .replace(".i/", ".i.hg/")
63 .replace(".d/", ".d.hg/"))
63 .replace(".d/", ".d.hg/"))
64
64
65 encodedir = getattr(parsers, 'encodedir', _encodedir)
65 encodedir = getattr(parsers, 'encodedir', _encodedir)
66
66
67 def decodedir(path):
67 def decodedir(path):
68 '''
68 '''
69 >>> decodedir(b'data/foo.i')
69 >>> decodedir(b'data/foo.i')
70 'data/foo.i'
70 'data/foo.i'
71 >>> decodedir(b'data/foo.i.hg/bla.i')
71 >>> decodedir(b'data/foo.i.hg/bla.i')
72 'data/foo.i/bla.i'
72 'data/foo.i/bla.i'
73 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
73 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
74 'data/foo.i.hg/bla.i'
74 'data/foo.i.hg/bla.i'
75 '''
75 '''
76 if ".hg/" not in path:
76 if ".hg/" not in path:
77 return path
77 return path
78 return (path
78 return (path
79 .replace(".d.hg/", ".d/")
79 .replace(".d.hg/", ".d/")
80 .replace(".i.hg/", ".i/")
80 .replace(".i.hg/", ".i/")
81 .replace(".hg.hg/", ".hg/"))
81 .replace(".hg.hg/", ".hg/"))
82
82
83 def _reserved():
83 def _reserved():
84 ''' characters that are problematic for filesystems
84 ''' characters that are problematic for filesystems
85
85
86 * ascii escapes (0..31)
86 * ascii escapes (0..31)
87 * ascii hi (126..255)
87 * ascii hi (126..255)
88 * windows specials
88 * windows specials
89
89
90 these characters will be escaped by encodefunctions
90 these characters will be escaped by encodefunctions
91 '''
91 '''
92 winreserved = [ord(x) for x in u'\\:*?"<>|']
92 winreserved = [ord(x) for x in u'\\:*?"<>|']
93 for x in range(32):
93 for x in range(32):
94 yield x
94 yield x
95 for x in range(126, 256):
95 for x in range(126, 256):
96 yield x
96 yield x
97 for x in winreserved:
97 for x in winreserved:
98 yield x
98 yield x
99
99
100 def _buildencodefun():
100 def _buildencodefun():
101 '''
101 '''
102 >>> enc, dec = _buildencodefun()
102 >>> enc, dec = _buildencodefun()
103
103
104 >>> enc(b'nothing/special.txt')
104 >>> enc(b'nothing/special.txt')
105 'nothing/special.txt'
105 'nothing/special.txt'
106 >>> dec(b'nothing/special.txt')
106 >>> dec(b'nothing/special.txt')
107 'nothing/special.txt'
107 'nothing/special.txt'
108
108
109 >>> enc(b'HELLO')
109 >>> enc(b'HELLO')
110 '_h_e_l_l_o'
110 '_h_e_l_l_o'
111 >>> dec(b'_h_e_l_l_o')
111 >>> dec(b'_h_e_l_l_o')
112 'HELLO'
112 'HELLO'
113
113
114 >>> enc(b'hello:world?')
114 >>> enc(b'hello:world?')
115 'hello~3aworld~3f'
115 'hello~3aworld~3f'
116 >>> dec(b'hello~3aworld~3f')
116 >>> dec(b'hello~3aworld~3f')
117 'hello:world?'
117 'hello:world?'
118
118
119 >>> enc(b'the\\x07quick\\xADshot')
119 >>> enc(b'the\\x07quick\\xADshot')
120 'the~07quick~adshot'
120 'the~07quick~adshot'
121 >>> dec(b'the~07quick~adshot')
121 >>> dec(b'the~07quick~adshot')
122 'the\\x07quick\\xadshot'
122 'the\\x07quick\\xadshot'
123 '''
123 '''
124 e = '_'
124 e = '_'
125 xchr = pycompat.bytechr
125 xchr = pycompat.bytechr
126 asciistr = list(map(xchr, range(127)))
126 asciistr = list(map(xchr, range(127)))
127 capitals = list(range(ord("A"), ord("Z") + 1))
127 capitals = list(range(ord("A"), ord("Z") + 1))
128
128
129 cmap = dict((x, x) for x in asciistr)
129 cmap = dict((x, x) for x in asciistr)
130 for x in _reserved():
130 for x in _reserved():
131 cmap[xchr(x)] = "~%02x" % x
131 cmap[xchr(x)] = "~%02x" % x
132 for x in capitals + [ord(e)]:
132 for x in capitals + [ord(e)]:
133 cmap[xchr(x)] = e + xchr(x).lower()
133 cmap[xchr(x)] = e + xchr(x).lower()
134
134
135 dmap = {}
135 dmap = {}
136 for k, v in cmap.iteritems():
136 for k, v in cmap.iteritems():
137 dmap[v] = k
137 dmap[v] = k
138 def decode(s):
138 def decode(s):
139 i = 0
139 i = 0
140 while i < len(s):
140 while i < len(s):
141 for l in pycompat.xrange(1, 4):
141 for l in pycompat.xrange(1, 4):
142 try:
142 try:
143 yield dmap[s[i:i + l]]
143 yield dmap[s[i:i + l]]
144 i += l
144 i += l
145 break
145 break
146 except KeyError:
146 except KeyError:
147 pass
147 pass
148 else:
148 else:
149 raise KeyError
149 raise KeyError
150 return (lambda s: ''.join([cmap[s[c:c + 1]]
150 return (lambda s: ''.join([cmap[s[c:c + 1]]
151 for c in pycompat.xrange(len(s))]),
151 for c in pycompat.xrange(len(s))]),
152 lambda s: ''.join(list(decode(s))))
152 lambda s: ''.join(list(decode(s))))
153
153
154 _encodefname, _decodefname = _buildencodefun()
154 _encodefname, _decodefname = _buildencodefun()
155
155
156 def encodefilename(s):
156 def encodefilename(s):
157 '''
157 '''
158 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
158 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
159 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
159 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
160 '''
160 '''
161 return _encodefname(encodedir(s))
161 return _encodefname(encodedir(s))
162
162
163 def decodefilename(s):
163 def decodefilename(s):
164 '''
164 '''
165 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
165 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
166 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
166 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
167 '''
167 '''
168 return decodedir(_decodefname(s))
168 return decodedir(_decodefname(s))
169
169
170 def _buildlowerencodefun():
170 def _buildlowerencodefun():
171 '''
171 '''
172 >>> f = _buildlowerencodefun()
172 >>> f = _buildlowerencodefun()
173 >>> f(b'nothing/special.txt')
173 >>> f(b'nothing/special.txt')
174 'nothing/special.txt'
174 'nothing/special.txt'
175 >>> f(b'HELLO')
175 >>> f(b'HELLO')
176 'hello'
176 'hello'
177 >>> f(b'hello:world?')
177 >>> f(b'hello:world?')
178 'hello~3aworld~3f'
178 'hello~3aworld~3f'
179 >>> f(b'the\\x07quick\\xADshot')
179 >>> f(b'the\\x07quick\\xADshot')
180 'the~07quick~adshot'
180 'the~07quick~adshot'
181 '''
181 '''
182 xchr = pycompat.bytechr
182 xchr = pycompat.bytechr
183 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
183 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
184 for x in _reserved():
184 for x in _reserved():
185 cmap[xchr(x)] = "~%02x" % x
185 cmap[xchr(x)] = "~%02x" % x
186 for x in range(ord("A"), ord("Z") + 1):
186 for x in range(ord("A"), ord("Z") + 1):
187 cmap[xchr(x)] = xchr(x).lower()
187 cmap[xchr(x)] = xchr(x).lower()
188 def lowerencode(s):
188 def lowerencode(s):
189 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
189 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
190 return lowerencode
190 return lowerencode
191
191
192 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
192 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
193
193
194 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
194 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
195 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
195 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
196 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
196 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
197 def _auxencode(path, dotencode):
197 def _auxencode(path, dotencode):
198 '''
198 '''
199 Encodes filenames containing names reserved by Windows or which end in
199 Encodes filenames containing names reserved by Windows or which end in
200 period or space. Does not touch other single reserved characters c.
200 period or space. Does not touch other single reserved characters c.
201 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
201 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
202 Additionally encodes space or period at the beginning, if dotencode is
202 Additionally encodes space or period at the beginning, if dotencode is
203 True. Parameter path is assumed to be all lowercase.
203 True. Parameter path is assumed to be all lowercase.
204 A segment only needs encoding if a reserved name appears as a
204 A segment only needs encoding if a reserved name appears as a
205 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
205 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
206 doesn't need encoding.
206 doesn't need encoding.
207
207
208 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
208 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
209 >>> _auxencode(s.split(b'/'), True)
209 >>> _auxencode(s.split(b'/'), True)
210 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
210 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
211 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
211 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
212 >>> _auxencode(s.split(b'/'), False)
212 >>> _auxencode(s.split(b'/'), False)
213 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
213 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
214 >>> _auxencode([b'foo. '], True)
214 >>> _auxencode([b'foo. '], True)
215 ['foo.~20']
215 ['foo.~20']
216 >>> _auxencode([b' .foo'], True)
216 >>> _auxencode([b' .foo'], True)
217 ['~20.foo']
217 ['~20.foo']
218 '''
218 '''
219 for i, n in enumerate(path):
219 for i, n in enumerate(path):
220 if not n:
220 if not n:
221 continue
221 continue
222 if dotencode and n[0] in '. ':
222 if dotencode and n[0] in '. ':
223 n = "~%02x" % ord(n[0:1]) + n[1:]
223 n = "~%02x" % ord(n[0:1]) + n[1:]
224 path[i] = n
224 path[i] = n
225 else:
225 else:
226 l = n.find('.')
226 l = n.find('.')
227 if l == -1:
227 if l == -1:
228 l = len(n)
228 l = len(n)
229 if ((l == 3 and n[:3] in _winres3) or
229 if ((l == 3 and n[:3] in _winres3) or
230 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
230 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
231 and n[:3] in _winres4)):
231 and n[:3] in _winres4)):
232 # encode third letter ('aux' -> 'au~78')
232 # encode third letter ('aux' -> 'au~78')
233 ec = "~%02x" % ord(n[2:3])
233 ec = "~%02x" % ord(n[2:3])
234 n = n[0:2] + ec + n[3:]
234 n = n[0:2] + ec + n[3:]
235 path[i] = n
235 path[i] = n
236 if n[-1] in '. ':
236 if n[-1] in '. ':
237 # encode last period or space ('foo...' -> 'foo..~2e')
237 # encode last period or space ('foo...' -> 'foo..~2e')
238 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
238 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
239 return path
239 return path
240
240
241 _maxstorepathlen = 120
241 _maxstorepathlen = 120
242 _dirprefixlen = 8
242 _dirprefixlen = 8
243 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
243 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
244
244
245 def _hashencode(path, dotencode):
245 def _hashencode(path, dotencode):
246 digest = node.hex(hashlib.sha1(path).digest())
246 digest = node.hex(hashlib.sha1(path).digest())
247 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
247 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
248 parts = _auxencode(le, dotencode)
248 parts = _auxencode(le, dotencode)
249 basename = parts[-1]
249 basename = parts[-1]
250 _root, ext = os.path.splitext(basename)
250 _root, ext = os.path.splitext(basename)
251 sdirs = []
251 sdirs = []
252 sdirslen = 0
252 sdirslen = 0
253 for p in parts[:-1]:
253 for p in parts[:-1]:
254 d = p[:_dirprefixlen]
254 d = p[:_dirprefixlen]
255 if d[-1] in '. ':
255 if d[-1] in '. ':
256 # Windows can't access dirs ending in period or space
256 # Windows can't access dirs ending in period or space
257 d = d[:-1] + '_'
257 d = d[:-1] + '_'
258 if sdirslen == 0:
258 if sdirslen == 0:
259 t = len(d)
259 t = len(d)
260 else:
260 else:
261 t = sdirslen + 1 + len(d)
261 t = sdirslen + 1 + len(d)
262 if t > _maxshortdirslen:
262 if t > _maxshortdirslen:
263 break
263 break
264 sdirs.append(d)
264 sdirs.append(d)
265 sdirslen = t
265 sdirslen = t
266 dirs = '/'.join(sdirs)
266 dirs = '/'.join(sdirs)
267 if len(dirs) > 0:
267 if len(dirs) > 0:
268 dirs += '/'
268 dirs += '/'
269 res = 'dh/' + dirs + digest + ext
269 res = 'dh/' + dirs + digest + ext
270 spaceleft = _maxstorepathlen - len(res)
270 spaceleft = _maxstorepathlen - len(res)
271 if spaceleft > 0:
271 if spaceleft > 0:
272 filler = basename[:spaceleft]
272 filler = basename[:spaceleft]
273 res = 'dh/' + dirs + filler + digest + ext
273 res = 'dh/' + dirs + filler + digest + ext
274 return res
274 return res
275
275
276 def _hybridencode(path, dotencode):
276 def _hybridencode(path, dotencode):
277 '''encodes path with a length limit
277 '''encodes path with a length limit
278
278
279 Encodes all paths that begin with 'data/', according to the following.
279 Encodes all paths that begin with 'data/', according to the following.
280
280
281 Default encoding (reversible):
281 Default encoding (reversible):
282
282
283 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
283 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
284 characters are encoded as '~xx', where xx is the two digit hex code
284 characters are encoded as '~xx', where xx is the two digit hex code
285 of the character (see encodefilename).
285 of the character (see encodefilename).
286 Relevant path components consisting of Windows reserved filenames are
286 Relevant path components consisting of Windows reserved filenames are
287 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
287 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
288
288
289 Hashed encoding (not reversible):
289 Hashed encoding (not reversible):
290
290
291 If the default-encoded path is longer than _maxstorepathlen, a
291 If the default-encoded path is longer than _maxstorepathlen, a
292 non-reversible hybrid hashing of the path is done instead.
292 non-reversible hybrid hashing of the path is done instead.
293 This encoding uses up to _dirprefixlen characters of all directory
293 This encoding uses up to _dirprefixlen characters of all directory
294 levels of the lowerencoded path, but not more levels than can fit into
294 levels of the lowerencoded path, but not more levels than can fit into
295 _maxshortdirslen.
295 _maxshortdirslen.
296 Then follows the filler followed by the sha digest of the full path.
296 Then follows the filler followed by the sha digest of the full path.
297 The filler is the beginning of the basename of the lowerencoded path
297 The filler is the beginning of the basename of the lowerencoded path
298 (the basename is everything after the last path separator). The filler
298 (the basename is everything after the last path separator). The filler
299 is as long as possible, filling in characters from the basename until
299 is as long as possible, filling in characters from the basename until
300 the encoded path has _maxstorepathlen characters (or all chars of the
300 the encoded path has _maxstorepathlen characters (or all chars of the
301 basename have been taken).
301 basename have been taken).
302 The extension (e.g. '.i' or '.d') is preserved.
302 The extension (e.g. '.i' or '.d') is preserved.
303
303
304 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
304 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
305 encoding was used.
305 encoding was used.
306 '''
306 '''
307 path = encodedir(path)
307 path = encodedir(path)
308 ef = _encodefname(path).split('/')
308 ef = _encodefname(path).split('/')
309 res = '/'.join(_auxencode(ef, dotencode))
309 res = '/'.join(_auxencode(ef, dotencode))
310 if len(res) > _maxstorepathlen:
310 if len(res) > _maxstorepathlen:
311 res = _hashencode(path, dotencode)
311 res = _hashencode(path, dotencode)
312 return res
312 return res
313
313
314 def _pathencode(path):
314 def _pathencode(path):
315 de = encodedir(path)
315 de = encodedir(path)
316 if len(path) > _maxstorepathlen:
316 if len(path) > _maxstorepathlen:
317 return _hashencode(de, True)
317 return _hashencode(de, True)
318 ef = _encodefname(de).split('/')
318 ef = _encodefname(de).split('/')
319 res = '/'.join(_auxencode(ef, True))
319 res = '/'.join(_auxencode(ef, True))
320 if len(res) > _maxstorepathlen:
320 if len(res) > _maxstorepathlen:
321 return _hashencode(de, True)
321 return _hashencode(de, True)
322 return res
322 return res
323
323
324 _pathencode = getattr(parsers, 'pathencode', _pathencode)
324 _pathencode = getattr(parsers, 'pathencode', _pathencode)
325
325
326 def _plainhybridencode(f):
326 def _plainhybridencode(f):
327 return _hybridencode(f, False)
327 return _hybridencode(f, False)
328
328
329 def _calcmode(vfs):
329 def _calcmode(vfs):
330 try:
330 try:
331 # files in .hg/ will be created using this mode
331 # files in .hg/ will be created using this mode
332 mode = vfs.stat().st_mode
332 mode = vfs.stat().st_mode
333 # avoid some useless chmods
333 # avoid some useless chmods
334 if (0o777 & ~util.umask) == (0o777 & mode):
334 if (0o777 & ~util.umask) == (0o777 & mode):
335 mode = None
335 mode = None
336 except OSError:
336 except OSError:
337 mode = None
337 mode = None
338 return mode
338 return mode
339
339
340 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
340 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
341 ' 00changelog.d 00changelog.i phaseroots obsstore')
341 ' 00changelog.d 00changelog.i phaseroots obsstore')
342
342
343 def isrevlog(f, kind, st):
343 def isrevlog(f, kind, st):
344 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
344 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
345
345
346 class basicstore(object):
346 class basicstore(object):
347 '''base class for local repository stores'''
347 '''base class for local repository stores'''
348 def __init__(self, path, vfstype):
348 def __init__(self, path, vfstype):
349 vfs = vfstype(path)
349 vfs = vfstype(path)
350 self.path = vfs.base
350 self.path = vfs.base
351 self.createmode = _calcmode(vfs)
351 self.createmode = _calcmode(vfs)
352 vfs.createmode = self.createmode
352 vfs.createmode = self.createmode
353 self.rawvfs = vfs
353 self.rawvfs = vfs
354 self.vfs = vfsmod.filtervfs(vfs, encodedir)
354 self.vfs = vfsmod.filtervfs(vfs, encodedir)
355 self.opener = self.vfs
355 self.opener = self.vfs
356
356
357 def join(self, f):
357 def join(self, f):
358 return self.path + '/' + encodedir(f)
358 return self.path + '/' + encodedir(f)
359
359
360 def _walk(self, relpath, recurse, filefilter=isrevlog):
360 def _walk(self, relpath, recurse, filefilter=isrevlog):
361 '''yields (unencoded, encoded, size)'''
361 '''yields (unencoded, encoded, size)'''
362 path = self.path
362 path = self.path
363 if relpath:
363 if relpath:
364 path += '/' + relpath
364 path += '/' + relpath
365 striplen = len(self.path) + 1
365 striplen = len(self.path) + 1
366 l = []
366 l = []
367 if self.rawvfs.isdir(path):
367 if self.rawvfs.isdir(path):
368 visit = [path]
368 visit = [path]
369 readdir = self.rawvfs.readdir
369 readdir = self.rawvfs.readdir
370 while visit:
370 while visit:
371 p = visit.pop()
371 p = visit.pop()
372 for f, kind, st in readdir(p, stat=True):
372 for f, kind, st in readdir(p, stat=True):
373 fp = p + '/' + f
373 fp = p + '/' + f
374 if filefilter(f, kind, st):
374 if filefilter(f, kind, st):
375 n = util.pconvert(fp[striplen:])
375 n = util.pconvert(fp[striplen:])
376 l.append((decodedir(n), n, st.st_size))
376 l.append((decodedir(n), n, st.st_size))
377 elif kind == stat.S_IFDIR and recurse:
377 elif kind == stat.S_IFDIR and recurse:
378 visit.append(fp)
378 visit.append(fp)
379 l.sort()
379 l.sort()
380 return l
380 return l
381
381
382 def datafiles(self, matcher=None):
382 def datafiles(self, matcher=None):
383 return self._walk('data', True) + self._walk('meta', True)
383 return self._walk('data', True) + self._walk('meta', True)
384
384
385 def topfiles(self):
385 def topfiles(self):
386 # yield manifest before changelog
386 # yield manifest before changelog
387 return reversed(self._walk('', False))
387 return reversed(self._walk('', False))
388
388
389 def walk(self, matcher=None):
389 def walk(self, matcher=None):
390 '''yields (unencoded, encoded, size)
390 '''yields (unencoded, encoded, size)
391
391
392 if a matcher is passed, storage files of only those tracked paths
392 if a matcher is passed, storage files of only those tracked paths
393 are passed with matches the matcher
393 are passed with matches the matcher
394 '''
394 '''
395 # yield data files first
395 # yield data files first
396 for x in self.datafiles(matcher):
396 for x in self.datafiles(matcher):
397 yield x
397 yield x
398 for x in self.topfiles():
398 for x in self.topfiles():
399 yield x
399 yield x
400
400
401 def copylist(self):
401 def copylist(self):
402 return ['requires'] + _data.split()
402 return ['requires'] + _data.split()
403
403
404 def write(self, tr):
404 def write(self, tr):
405 pass
405 pass
406
406
407 def invalidatecaches(self):
407 def invalidatecaches(self):
408 pass
408 pass
409
409
410 def markremoved(self, fn):
410 def markremoved(self, fn):
411 pass
411 pass
412
412
413 def __contains__(self, path):
413 def __contains__(self, path):
414 '''Checks if the store contains path'''
414 '''Checks if the store contains path'''
415 path = "/".join(("data", path))
415 path = "/".join(("data", path))
416 # file?
416 # file?
417 if self.vfs.exists(path + ".i"):
417 if self.vfs.exists(path + ".i"):
418 return True
418 return True
419 # dir?
419 # dir?
420 if not path.endswith("/"):
420 if not path.endswith("/"):
421 path = path + "/"
421 path = path + "/"
422 return self.vfs.exists(path)
422 return self.vfs.exists(path)
423
423
424 class encodedstore(basicstore):
424 class encodedstore(basicstore):
425 def __init__(self, path, vfstype):
425 def __init__(self, path, vfstype):
426 vfs = vfstype(path + '/store')
426 vfs = vfstype(path + '/store')
427 self.path = vfs.base
427 self.path = vfs.base
428 self.createmode = _calcmode(vfs)
428 self.createmode = _calcmode(vfs)
429 vfs.createmode = self.createmode
429 vfs.createmode = self.createmode
430 self.rawvfs = vfs
430 self.rawvfs = vfs
431 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
431 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
432 self.opener = self.vfs
432 self.opener = self.vfs
433
433
434 def datafiles(self, matcher=None):
434 def datafiles(self, matcher=None):
435 for a, b, size in super(encodedstore, self).datafiles():
435 for a, b, size in super(encodedstore, self).datafiles():
436 try:
436 try:
437 a = decodefilename(a)
437 a = decodefilename(a)
438 except KeyError:
438 except KeyError:
439 a = None
439 a = None
440 if a is not None and not _matchtrackedpath(a, matcher):
440 if a is not None and not _matchtrackedpath(a, matcher):
441 continue
441 continue
442 yield a, b, size
442 yield a, b, size
443
443
444 def join(self, f):
444 def join(self, f):
445 return self.path + '/' + encodefilename(f)
445 return self.path + '/' + encodefilename(f)
446
446
447 def copylist(self):
447 def copylist(self):
448 return (['requires', '00changelog.i'] +
448 return (['requires', '00changelog.i'] +
449 ['store/' + f for f in _data.split()])
449 ['store/' + f for f in _data.split()])
450
450
451 class fncache(object):
451 class fncache(object):
452 # the filename used to be partially encoded
452 # the filename used to be partially encoded
453 # hence the encodedir/decodedir dance
453 # hence the encodedir/decodedir dance
454 def __init__(self, vfs):
454 def __init__(self, vfs):
455 self.vfs = vfs
455 self.vfs = vfs
456 self.entries = None
456 self.entries = None
457 self._dirty = False
457 self._dirty = False
458 # set of new additions to fncache
458 # set of new additions to fncache
459 self.addls = set()
459 self.addls = set()
460
460
461 def _load(self):
461 def _load(self):
462 '''fill the entries from the fncache file'''
462 '''fill the entries from the fncache file'''
463 self._dirty = False
463 self._dirty = False
464 try:
464 try:
465 fp = self.vfs('fncache', mode='rb')
465 fp = self.vfs('fncache', mode='rb')
466 except IOError:
466 except IOError:
467 # skip nonexistent file
467 # skip nonexistent file
468 self.entries = set()
468 self.entries = set()
469 return
469 return
470
470
471 self.entries = set()
471 self.entries = set()
472 chunk = b''
472 chunk = b''
473 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
473 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
474 chunk += c
474 chunk += c
475 try:
475 try:
476 p = chunk.rindex(b'\n')
476 p = chunk.rindex(b'\n')
477 self.entries.update(decodedir(chunk[:p + 1]).splitlines())
477 self.entries.update(decodedir(chunk[:p + 1]).splitlines())
478 chunk = chunk[p + 1:]
478 chunk = chunk[p + 1:]
479 except ValueError:
479 except ValueError:
480 # substring '\n' not found, maybe the entry is bigger than the
480 # substring '\n' not found, maybe the entry is bigger than the
481 # chunksize, so let's keep iterating
481 # chunksize, so let's keep iterating
482 pass
482 pass
483
483
484 if chunk:
484 if chunk:
485 raise error.Abort(_("fncache does not ends with a newline"))
485 raise error.Abort(_("fncache does not ends with a newline"),
486 hint=_("use 'hg debugrebuildfncache' to rebuild"
487 " the fncache"))
486 self._checkentries(fp)
488 self._checkentries(fp)
487 fp.close()
489 fp.close()
488
490
489 def _checkentries(self, fp):
491 def _checkentries(self, fp):
490 """ make sure there is no empty string in entries """
492 """ make sure there is no empty string in entries """
491 if '' in self.entries:
493 if '' in self.entries:
492 fp.seek(0)
494 fp.seek(0)
493 for n, line in enumerate(util.iterfile(fp)):
495 for n, line in enumerate(util.iterfile(fp)):
494 if not line.rstrip('\n'):
496 if not line.rstrip('\n'):
495 t = _('invalid entry in fncache, line %d') % (n + 1)
497 t = _('invalid entry in fncache, line %d') % (n + 1)
496 raise error.Abort(t)
498 raise error.Abort(t)
497
499
498 def write(self, tr):
500 def write(self, tr):
499 if self._dirty:
501 if self._dirty:
500 assert self.entries is not None
502 assert self.entries is not None
501 self.entries = self.entries | self.addls
503 self.entries = self.entries | self.addls
502 self.addls = set()
504 self.addls = set()
503 tr.addbackup('fncache')
505 tr.addbackup('fncache')
504 fp = self.vfs('fncache', mode='wb', atomictemp=True)
506 fp = self.vfs('fncache', mode='wb', atomictemp=True)
505 if self.entries:
507 if self.entries:
506 fp.write(encodedir('\n'.join(self.entries) + '\n'))
508 fp.write(encodedir('\n'.join(self.entries) + '\n'))
507 fp.close()
509 fp.close()
508 self._dirty = False
510 self._dirty = False
509 if self.addls:
511 if self.addls:
510 # if we have just new entries, let's append them to the fncache
512 # if we have just new entries, let's append them to the fncache
511 tr.addbackup('fncache')
513 tr.addbackup('fncache')
512 fp = self.vfs('fncache', mode='ab', atomictemp=True)
514 fp = self.vfs('fncache', mode='ab', atomictemp=True)
513 if self.addls:
515 if self.addls:
514 fp.write(encodedir('\n'.join(self.addls) + '\n'))
516 fp.write(encodedir('\n'.join(self.addls) + '\n'))
515 fp.close()
517 fp.close()
516 self.entries = None
518 self.entries = None
517 self.addls = set()
519 self.addls = set()
518
520
519 def add(self, fn):
521 def add(self, fn):
520 if self.entries is None:
522 if self.entries is None:
521 self._load()
523 self._load()
522 if fn not in self.entries:
524 if fn not in self.entries:
523 self.addls.add(fn)
525 self.addls.add(fn)
524
526
525 def remove(self, fn):
527 def remove(self, fn):
526 if self.entries is None:
528 if self.entries is None:
527 self._load()
529 self._load()
528 if fn in self.addls:
530 if fn in self.addls:
529 self.addls.remove(fn)
531 self.addls.remove(fn)
530 return
532 return
531 try:
533 try:
532 self.entries.remove(fn)
534 self.entries.remove(fn)
533 self._dirty = True
535 self._dirty = True
534 except KeyError:
536 except KeyError:
535 pass
537 pass
536
538
537 def __contains__(self, fn):
539 def __contains__(self, fn):
538 if fn in self.addls:
540 if fn in self.addls:
539 return True
541 return True
540 if self.entries is None:
542 if self.entries is None:
541 self._load()
543 self._load()
542 return fn in self.entries
544 return fn in self.entries
543
545
544 def __iter__(self):
546 def __iter__(self):
545 if self.entries is None:
547 if self.entries is None:
546 self._load()
548 self._load()
547 return iter(self.entries | self.addls)
549 return iter(self.entries | self.addls)
548
550
549 class _fncachevfs(vfsmod.proxyvfs):
551 class _fncachevfs(vfsmod.proxyvfs):
550 def __init__(self, vfs, fnc, encode):
552 def __init__(self, vfs, fnc, encode):
551 vfsmod.proxyvfs.__init__(self, vfs)
553 vfsmod.proxyvfs.__init__(self, vfs)
552 self.fncache = fnc
554 self.fncache = fnc
553 self.encode = encode
555 self.encode = encode
554
556
555 def __call__(self, path, mode='r', *args, **kw):
557 def __call__(self, path, mode='r', *args, **kw):
556 encoded = self.encode(path)
558 encoded = self.encode(path)
557 if mode not in ('r', 'rb') and (path.startswith('data/') or
559 if mode not in ('r', 'rb') and (path.startswith('data/') or
558 path.startswith('meta/')):
560 path.startswith('meta/')):
559 # do not trigger a fncache load when adding a file that already is
561 # do not trigger a fncache load when adding a file that already is
560 # known to exist.
562 # known to exist.
561 notload = self.fncache.entries is None and self.vfs.exists(encoded)
563 notload = self.fncache.entries is None and self.vfs.exists(encoded)
562 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
564 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
563 # when appending to an existing file, if the file has size zero,
565 # when appending to an existing file, if the file has size zero,
564 # it should be considered as missing. Such zero-size files are
566 # it should be considered as missing. Such zero-size files are
565 # the result of truncation when a transaction is aborted.
567 # the result of truncation when a transaction is aborted.
566 notload = False
568 notload = False
567 if not notload:
569 if not notload:
568 self.fncache.add(path)
570 self.fncache.add(path)
569 return self.vfs(encoded, mode, *args, **kw)
571 return self.vfs(encoded, mode, *args, **kw)
570
572
571 def join(self, path):
573 def join(self, path):
572 if path:
574 if path:
573 return self.vfs.join(self.encode(path))
575 return self.vfs.join(self.encode(path))
574 else:
576 else:
575 return self.vfs.join(path)
577 return self.vfs.join(path)
576
578
577 class fncachestore(basicstore):
579 class fncachestore(basicstore):
578 def __init__(self, path, vfstype, dotencode):
580 def __init__(self, path, vfstype, dotencode):
579 if dotencode:
581 if dotencode:
580 encode = _pathencode
582 encode = _pathencode
581 else:
583 else:
582 encode = _plainhybridencode
584 encode = _plainhybridencode
583 self.encode = encode
585 self.encode = encode
584 vfs = vfstype(path + '/store')
586 vfs = vfstype(path + '/store')
585 self.path = vfs.base
587 self.path = vfs.base
586 self.pathsep = self.path + '/'
588 self.pathsep = self.path + '/'
587 self.createmode = _calcmode(vfs)
589 self.createmode = _calcmode(vfs)
588 vfs.createmode = self.createmode
590 vfs.createmode = self.createmode
589 self.rawvfs = vfs
591 self.rawvfs = vfs
590 fnc = fncache(vfs)
592 fnc = fncache(vfs)
591 self.fncache = fnc
593 self.fncache = fnc
592 self.vfs = _fncachevfs(vfs, fnc, encode)
594 self.vfs = _fncachevfs(vfs, fnc, encode)
593 self.opener = self.vfs
595 self.opener = self.vfs
594
596
595 def join(self, f):
597 def join(self, f):
596 return self.pathsep + self.encode(f)
598 return self.pathsep + self.encode(f)
597
599
598 def getsize(self, path):
600 def getsize(self, path):
599 return self.rawvfs.stat(path).st_size
601 return self.rawvfs.stat(path).st_size
600
602
601 def datafiles(self, matcher=None):
603 def datafiles(self, matcher=None):
602 for f in sorted(self.fncache):
604 for f in sorted(self.fncache):
603 if not _matchtrackedpath(f, matcher):
605 if not _matchtrackedpath(f, matcher):
604 continue
606 continue
605 ef = self.encode(f)
607 ef = self.encode(f)
606 try:
608 try:
607 yield f, ef, self.getsize(ef)
609 yield f, ef, self.getsize(ef)
608 except OSError as err:
610 except OSError as err:
609 if err.errno != errno.ENOENT:
611 if err.errno != errno.ENOENT:
610 raise
612 raise
611
613
612 def copylist(self):
614 def copylist(self):
613 d = ('narrowspec data meta dh fncache phaseroots obsstore'
615 d = ('narrowspec data meta dh fncache phaseroots obsstore'
614 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
616 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
615 return (['requires', '00changelog.i'] +
617 return (['requires', '00changelog.i'] +
616 ['store/' + f for f in d.split()])
618 ['store/' + f for f in d.split()])
617
619
618 def write(self, tr):
620 def write(self, tr):
619 self.fncache.write(tr)
621 self.fncache.write(tr)
620
622
621 def invalidatecaches(self):
623 def invalidatecaches(self):
622 self.fncache.entries = None
624 self.fncache.entries = None
623 self.fncache.addls = set()
625 self.fncache.addls = set()
624
626
625 def markremoved(self, fn):
627 def markremoved(self, fn):
626 self.fncache.remove(fn)
628 self.fncache.remove(fn)
627
629
628 def _exists(self, f):
630 def _exists(self, f):
629 ef = self.encode(f)
631 ef = self.encode(f)
630 try:
632 try:
631 self.getsize(ef)
633 self.getsize(ef)
632 return True
634 return True
633 except OSError as err:
635 except OSError as err:
634 if err.errno != errno.ENOENT:
636 if err.errno != errno.ENOENT:
635 raise
637 raise
636 # nonexistent entry
638 # nonexistent entry
637 return False
639 return False
638
640
639 def __contains__(self, path):
641 def __contains__(self, path):
640 '''Checks if the store contains path'''
642 '''Checks if the store contains path'''
641 path = "/".join(("data", path))
643 path = "/".join(("data", path))
642 # check for files (exact match)
644 # check for files (exact match)
643 e = path + '.i'
645 e = path + '.i'
644 if e in self.fncache and self._exists(e):
646 if e in self.fncache and self._exists(e):
645 return True
647 return True
646 # now check for directories (prefix match)
648 # now check for directories (prefix match)
647 if not path.endswith('/'):
649 if not path.endswith('/'):
648 path += '/'
650 path += '/'
649 for e in self.fncache:
651 for e in self.fncache:
650 if e.startswith(path) and self._exists(e):
652 if e.startswith(path) and self._exists(e):
651 return True
653 return True
652 return False
654 return False
General Comments 0
You need to be logged in to leave comments. Login now