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