##// END OF EJS Templates
store: fix a signature mismatch for a vfs subclass...
Matt Harbison -
r52775:2391a5fa default
parent child Browse files
Show More
@@ -1,1252 +1,1258
1 # store.py - repository store handling for Mercurial)
1 # store.py - repository store handling for Mercurial)
2 #
2 #
3 # Copyright 2008 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2008 Olivia Mackall <olivia@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 annotations
8 from __future__ import annotations
9
9
10 import collections
10 import collections
11 import functools
11 import functools
12 import os
12 import os
13 import re
13 import re
14 import stat
14 import stat
15 import typing
15 import typing
16
16
17 from typing import Generator, List
17 from typing import (
18 Generator,
19 List,
20 Optional,
21 )
18
22
19 from .i18n import _
23 from .i18n import _
20 from .thirdparty import attr
24 from .thirdparty import attr
21
25
22 # Force pytype to use the non-vendored package
26 # Force pytype to use the non-vendored package
23 if typing.TYPE_CHECKING:
27 if typing.TYPE_CHECKING:
24 # noinspection PyPackageRequirements
28 # noinspection PyPackageRequirements
25 import attr
29 import attr
26
30
27 from .node import hex
31 from .node import hex
28 from .revlogutils.constants import (
32 from .revlogutils.constants import (
29 INDEX_HEADER,
33 INDEX_HEADER,
30 KIND_CHANGELOG,
34 KIND_CHANGELOG,
31 KIND_FILELOG,
35 KIND_FILELOG,
32 KIND_MANIFESTLOG,
36 KIND_MANIFESTLOG,
33 )
37 )
34 from . import (
38 from . import (
35 changelog,
39 changelog,
36 error,
40 error,
37 filelog,
41 filelog,
38 manifest,
42 manifest,
39 policy,
43 policy,
40 pycompat,
44 pycompat,
41 revlog as revlogmod,
45 revlog as revlogmod,
42 util,
46 util,
43 vfs as vfsmod,
47 vfs as vfsmod,
44 )
48 )
45 from .utils import hashutil
49 from .utils import hashutil
46
50
47 parsers = policy.importmod('parsers')
51 parsers = policy.importmod('parsers')
48 # how much bytes should be read from fncache in one read
52 # how much bytes should be read from fncache in one read
49 # It is done to prevent loading large fncache files into memory
53 # It is done to prevent loading large fncache files into memory
50 fncache_chunksize = 10**6
54 fncache_chunksize = 10**6
51
55
52
56
53 def _match_tracked_entry(entry: "BaseStoreEntry", matcher):
57 def _match_tracked_entry(entry: "BaseStoreEntry", matcher):
54 """parses a fncache entry and returns whether the entry is tracking a path
58 """parses a fncache entry and returns whether the entry is tracking a path
55 matched by matcher or not.
59 matched by matcher or not.
56
60
57 If matcher is None, returns True"""
61 If matcher is None, returns True"""
58
62
59 if matcher is None:
63 if matcher is None:
60 return True
64 return True
61
65
62 # TODO: make this safe for other entry types. Currently, the various
66 # TODO: make this safe for other entry types. Currently, the various
63 # store.data_entry generators only yield RevlogStoreEntry, so the
67 # store.data_entry generators only yield RevlogStoreEntry, so the
64 # attributes do exist on `entry`.
68 # attributes do exist on `entry`.
65 # pytype: disable=attribute-error
69 # pytype: disable=attribute-error
66 if entry.is_filelog:
70 if entry.is_filelog:
67 return matcher(entry.target_id)
71 return matcher(entry.target_id)
68 elif entry.is_manifestlog:
72 elif entry.is_manifestlog:
69 return matcher.visitdir(entry.target_id.rstrip(b'/'))
73 return matcher.visitdir(entry.target_id.rstrip(b'/'))
70 # pytype: enable=attribute-error
74 # pytype: enable=attribute-error
71 raise error.ProgrammingError(b"cannot process entry %r" % entry)
75 raise error.ProgrammingError(b"cannot process entry %r" % entry)
72
76
73
77
74 # This avoids a collision between a file named foo and a dir named
78 # This avoids a collision between a file named foo and a dir named
75 # foo.i or foo.d
79 # foo.i or foo.d
76 def _encodedir(path):
80 def _encodedir(path):
77 """
81 """
78 >>> _encodedir(b'data/foo.i')
82 >>> _encodedir(b'data/foo.i')
79 'data/foo.i'
83 'data/foo.i'
80 >>> _encodedir(b'data/foo.i/bla.i')
84 >>> _encodedir(b'data/foo.i/bla.i')
81 'data/foo.i.hg/bla.i'
85 'data/foo.i.hg/bla.i'
82 >>> _encodedir(b'data/foo.i.hg/bla.i')
86 >>> _encodedir(b'data/foo.i.hg/bla.i')
83 'data/foo.i.hg.hg/bla.i'
87 'data/foo.i.hg.hg/bla.i'
84 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
88 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
85 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
89 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
86 """
90 """
87 return (
91 return (
88 path.replace(b".hg/", b".hg.hg/")
92 path.replace(b".hg/", b".hg.hg/")
89 .replace(b".i/", b".i.hg/")
93 .replace(b".i/", b".i.hg/")
90 .replace(b".d/", b".d.hg/")
94 .replace(b".d/", b".d.hg/")
91 )
95 )
92
96
93
97
94 encodedir = getattr(parsers, 'encodedir', _encodedir)
98 encodedir = getattr(parsers, 'encodedir', _encodedir)
95
99
96
100
97 def decodedir(path):
101 def decodedir(path):
98 """
102 """
99 >>> decodedir(b'data/foo.i')
103 >>> decodedir(b'data/foo.i')
100 'data/foo.i'
104 'data/foo.i'
101 >>> decodedir(b'data/foo.i.hg/bla.i')
105 >>> decodedir(b'data/foo.i.hg/bla.i')
102 'data/foo.i/bla.i'
106 'data/foo.i/bla.i'
103 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
107 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
104 'data/foo.i.hg/bla.i'
108 'data/foo.i.hg/bla.i'
105 """
109 """
106 if b".hg/" not in path:
110 if b".hg/" not in path:
107 return path
111 return path
108 return (
112 return (
109 path.replace(b".d.hg/", b".d/")
113 path.replace(b".d.hg/", b".d/")
110 .replace(b".i.hg/", b".i/")
114 .replace(b".i.hg/", b".i/")
111 .replace(b".hg.hg/", b".hg/")
115 .replace(b".hg.hg/", b".hg/")
112 )
116 )
113
117
114
118
115 def _reserved():
119 def _reserved():
116 """characters that are problematic for filesystems
120 """characters that are problematic for filesystems
117
121
118 * ascii escapes (0..31)
122 * ascii escapes (0..31)
119 * ascii hi (126..255)
123 * ascii hi (126..255)
120 * windows specials
124 * windows specials
121
125
122 these characters will be escaped by encodefunctions
126 these characters will be escaped by encodefunctions
123 """
127 """
124 winreserved = [ord(x) for x in u'\\:*?"<>|']
128 winreserved = [ord(x) for x in u'\\:*?"<>|']
125 for x in range(32):
129 for x in range(32):
126 yield x
130 yield x
127 for x in range(126, 256):
131 for x in range(126, 256):
128 yield x
132 yield x
129 for x in winreserved:
133 for x in winreserved:
130 yield x
134 yield x
131
135
132
136
133 def _buildencodefun():
137 def _buildencodefun():
134 """
138 """
135 >>> enc, dec = _buildencodefun()
139 >>> enc, dec = _buildencodefun()
136
140
137 >>> enc(b'nothing/special.txt')
141 >>> enc(b'nothing/special.txt')
138 'nothing/special.txt'
142 'nothing/special.txt'
139 >>> dec(b'nothing/special.txt')
143 >>> dec(b'nothing/special.txt')
140 'nothing/special.txt'
144 'nothing/special.txt'
141
145
142 >>> enc(b'HELLO')
146 >>> enc(b'HELLO')
143 '_h_e_l_l_o'
147 '_h_e_l_l_o'
144 >>> dec(b'_h_e_l_l_o')
148 >>> dec(b'_h_e_l_l_o')
145 'HELLO'
149 'HELLO'
146
150
147 >>> enc(b'hello:world?')
151 >>> enc(b'hello:world?')
148 'hello~3aworld~3f'
152 'hello~3aworld~3f'
149 >>> dec(b'hello~3aworld~3f')
153 >>> dec(b'hello~3aworld~3f')
150 'hello:world?'
154 'hello:world?'
151
155
152 >>> enc(b'the\\x07quick\\xADshot')
156 >>> enc(b'the\\x07quick\\xADshot')
153 'the~07quick~adshot'
157 'the~07quick~adshot'
154 >>> dec(b'the~07quick~adshot')
158 >>> dec(b'the~07quick~adshot')
155 'the\\x07quick\\xadshot'
159 'the\\x07quick\\xadshot'
156 """
160 """
157 e = b'_'
161 e = b'_'
158 xchr = pycompat.bytechr
162 xchr = pycompat.bytechr
159 asciistr = list(map(xchr, range(127)))
163 asciistr = list(map(xchr, range(127)))
160 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
164 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
161
165
162 cmap = {x: x for x in asciistr}
166 cmap = {x: x for x in asciistr}
163 for x in _reserved():
167 for x in _reserved():
164 cmap[xchr(x)] = b"~%02x" % x
168 cmap[xchr(x)] = b"~%02x" % x
165 for x in capitals + [ord(e)]:
169 for x in capitals + [ord(e)]:
166 cmap[xchr(x)] = e + xchr(x).lower()
170 cmap[xchr(x)] = e + xchr(x).lower()
167
171
168 dmap = {}
172 dmap = {}
169 for k, v in cmap.items():
173 for k, v in cmap.items():
170 dmap[v] = k
174 dmap[v] = k
171
175
172 def decode(s):
176 def decode(s):
173 i = 0
177 i = 0
174 while i < len(s):
178 while i < len(s):
175 for l in range(1, 4):
179 for l in range(1, 4):
176 try:
180 try:
177 yield dmap[s[i : i + l]]
181 yield dmap[s[i : i + l]]
178 i += l
182 i += l
179 break
183 break
180 except KeyError:
184 except KeyError:
181 pass
185 pass
182 else:
186 else:
183 raise KeyError
187 raise KeyError
184
188
185 return (
189 return (
186 lambda s: b''.join([cmap[s[c : c + 1]] for c in range(len(s))]),
190 lambda s: b''.join([cmap[s[c : c + 1]] for c in range(len(s))]),
187 lambda s: b''.join(list(decode(s))),
191 lambda s: b''.join(list(decode(s))),
188 )
192 )
189
193
190
194
191 _encodefname, _decodefname = _buildencodefun()
195 _encodefname, _decodefname = _buildencodefun()
192
196
193
197
194 def encodefilename(s):
198 def encodefilename(s):
195 """
199 """
196 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
200 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
197 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
201 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
198 """
202 """
199 return _encodefname(encodedir(s))
203 return _encodefname(encodedir(s))
200
204
201
205
202 def decodefilename(s):
206 def decodefilename(s):
203 """
207 """
204 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
208 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
205 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
209 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
206 """
210 """
207 return decodedir(_decodefname(s))
211 return decodedir(_decodefname(s))
208
212
209
213
210 def _buildlowerencodefun():
214 def _buildlowerencodefun():
211 """
215 """
212 >>> f = _buildlowerencodefun()
216 >>> f = _buildlowerencodefun()
213 >>> f(b'nothing/special.txt')
217 >>> f(b'nothing/special.txt')
214 'nothing/special.txt'
218 'nothing/special.txt'
215 >>> f(b'HELLO')
219 >>> f(b'HELLO')
216 'hello'
220 'hello'
217 >>> f(b'hello:world?')
221 >>> f(b'hello:world?')
218 'hello~3aworld~3f'
222 'hello~3aworld~3f'
219 >>> f(b'the\\x07quick\\xADshot')
223 >>> f(b'the\\x07quick\\xADshot')
220 'the~07quick~adshot'
224 'the~07quick~adshot'
221 """
225 """
222 xchr = pycompat.bytechr
226 xchr = pycompat.bytechr
223 cmap = {xchr(x): xchr(x) for x in range(127)}
227 cmap = {xchr(x): xchr(x) for x in range(127)}
224 for x in _reserved():
228 for x in _reserved():
225 cmap[xchr(x)] = b"~%02x" % x
229 cmap[xchr(x)] = b"~%02x" % x
226 for x in range(ord(b"A"), ord(b"Z") + 1):
230 for x in range(ord(b"A"), ord(b"Z") + 1):
227 cmap[xchr(x)] = xchr(x).lower()
231 cmap[xchr(x)] = xchr(x).lower()
228
232
229 def lowerencode(s):
233 def lowerencode(s):
230 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
234 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
231
235
232 return lowerencode
236 return lowerencode
233
237
234
238
235 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
239 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
236
240
237 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
241 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
238 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
242 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
239 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
243 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
240
244
241
245
242 def _auxencode(path, dotencode):
246 def _auxencode(path, dotencode):
243 """
247 """
244 Encodes filenames containing names reserved by Windows or which end in
248 Encodes filenames containing names reserved by Windows or which end in
245 period or space. Does not touch other single reserved characters c.
249 period or space. Does not touch other single reserved characters c.
246 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
250 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
247 Additionally encodes space or period at the beginning, if dotencode is
251 Additionally encodes space or period at the beginning, if dotencode is
248 True. Parameter path is assumed to be all lowercase.
252 True. Parameter path is assumed to be all lowercase.
249 A segment only needs encoding if a reserved name appears as a
253 A segment only needs encoding if a reserved name appears as a
250 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
254 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
251 doesn't need encoding.
255 doesn't need encoding.
252
256
253 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
257 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
254 >>> _auxencode(s.split(b'/'), True)
258 >>> _auxencode(s.split(b'/'), True)
255 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
259 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
256 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
260 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
257 >>> _auxencode(s.split(b'/'), False)
261 >>> _auxencode(s.split(b'/'), False)
258 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
262 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
259 >>> _auxencode([b'foo. '], True)
263 >>> _auxencode([b'foo. '], True)
260 ['foo.~20']
264 ['foo.~20']
261 >>> _auxencode([b' .foo'], True)
265 >>> _auxencode([b' .foo'], True)
262 ['~20.foo']
266 ['~20.foo']
263 """
267 """
264 for i, n in enumerate(path):
268 for i, n in enumerate(path):
265 if not n:
269 if not n:
266 continue
270 continue
267 if dotencode and n[0] in b'. ':
271 if dotencode and n[0] in b'. ':
268 n = b"~%02x" % ord(n[0:1]) + n[1:]
272 n = b"~%02x" % ord(n[0:1]) + n[1:]
269 path[i] = n
273 path[i] = n
270 else:
274 else:
271 l = n.find(b'.')
275 l = n.find(b'.')
272 if l == -1:
276 if l == -1:
273 l = len(n)
277 l = len(n)
274 if (l == 3 and n[:3] in _winres3) or (
278 if (l == 3 and n[:3] in _winres3) or (
275 l == 4
279 l == 4
276 and n[3:4] <= b'9'
280 and n[3:4] <= b'9'
277 and n[3:4] >= b'1'
281 and n[3:4] >= b'1'
278 and n[:3] in _winres4
282 and n[:3] in _winres4
279 ):
283 ):
280 # encode third letter ('aux' -> 'au~78')
284 # encode third letter ('aux' -> 'au~78')
281 ec = b"~%02x" % ord(n[2:3])
285 ec = b"~%02x" % ord(n[2:3])
282 n = n[0:2] + ec + n[3:]
286 n = n[0:2] + ec + n[3:]
283 path[i] = n
287 path[i] = n
284 if n[-1] in b'. ':
288 if n[-1] in b'. ':
285 # encode last period or space ('foo...' -> 'foo..~2e')
289 # encode last period or space ('foo...' -> 'foo..~2e')
286 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
290 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
287 return path
291 return path
288
292
289
293
290 _maxstorepathlen = 120
294 _maxstorepathlen = 120
291 _dirprefixlen = 8
295 _dirprefixlen = 8
292 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
296 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
293
297
294
298
295 def _hashencode(path, dotencode):
299 def _hashencode(path, dotencode):
296 digest = hex(hashutil.sha1(path).digest())
300 digest = hex(hashutil.sha1(path).digest())
297 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
301 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
298 parts = _auxencode(le, dotencode)
302 parts = _auxencode(le, dotencode)
299 basename = parts[-1]
303 basename = parts[-1]
300 _root, ext = os.path.splitext(basename)
304 _root, ext = os.path.splitext(basename)
301 sdirs = []
305 sdirs = []
302 sdirslen = 0
306 sdirslen = 0
303 for p in parts[:-1]:
307 for p in parts[:-1]:
304 d = p[:_dirprefixlen]
308 d = p[:_dirprefixlen]
305 if d[-1] in b'. ':
309 if d[-1] in b'. ':
306 # Windows can't access dirs ending in period or space
310 # Windows can't access dirs ending in period or space
307 d = d[:-1] + b'_'
311 d = d[:-1] + b'_'
308 if sdirslen == 0:
312 if sdirslen == 0:
309 t = len(d)
313 t = len(d)
310 else:
314 else:
311 t = sdirslen + 1 + len(d)
315 t = sdirslen + 1 + len(d)
312 if t > _maxshortdirslen:
316 if t > _maxshortdirslen:
313 break
317 break
314 sdirs.append(d)
318 sdirs.append(d)
315 sdirslen = t
319 sdirslen = t
316 dirs = b'/'.join(sdirs)
320 dirs = b'/'.join(sdirs)
317 if len(dirs) > 0:
321 if len(dirs) > 0:
318 dirs += b'/'
322 dirs += b'/'
319 res = b'dh/' + dirs + digest + ext
323 res = b'dh/' + dirs + digest + ext
320 spaceleft = _maxstorepathlen - len(res)
324 spaceleft = _maxstorepathlen - len(res)
321 if spaceleft > 0:
325 if spaceleft > 0:
322 filler = basename[:spaceleft]
326 filler = basename[:spaceleft]
323 res = b'dh/' + dirs + filler + digest + ext
327 res = b'dh/' + dirs + filler + digest + ext
324 return res
328 return res
325
329
326
330
327 def _hybridencode(path, dotencode):
331 def _hybridencode(path, dotencode):
328 """encodes path with a length limit
332 """encodes path with a length limit
329
333
330 Encodes all paths that begin with 'data/', according to the following.
334 Encodes all paths that begin with 'data/', according to the following.
331
335
332 Default encoding (reversible):
336 Default encoding (reversible):
333
337
334 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
338 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
335 characters are encoded as '~xx', where xx is the two digit hex code
339 characters are encoded as '~xx', where xx is the two digit hex code
336 of the character (see encodefilename).
340 of the character (see encodefilename).
337 Relevant path components consisting of Windows reserved filenames are
341 Relevant path components consisting of Windows reserved filenames are
338 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
342 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
339
343
340 Hashed encoding (not reversible):
344 Hashed encoding (not reversible):
341
345
342 If the default-encoded path is longer than _maxstorepathlen, a
346 If the default-encoded path is longer than _maxstorepathlen, a
343 non-reversible hybrid hashing of the path is done instead.
347 non-reversible hybrid hashing of the path is done instead.
344 This encoding uses up to _dirprefixlen characters of all directory
348 This encoding uses up to _dirprefixlen characters of all directory
345 levels of the lowerencoded path, but not more levels than can fit into
349 levels of the lowerencoded path, but not more levels than can fit into
346 _maxshortdirslen.
350 _maxshortdirslen.
347 Then follows the filler followed by the sha digest of the full path.
351 Then follows the filler followed by the sha digest of the full path.
348 The filler is the beginning of the basename of the lowerencoded path
352 The filler is the beginning of the basename of the lowerencoded path
349 (the basename is everything after the last path separator). The filler
353 (the basename is everything after the last path separator). The filler
350 is as long as possible, filling in characters from the basename until
354 is as long as possible, filling in characters from the basename until
351 the encoded path has _maxstorepathlen characters (or all chars of the
355 the encoded path has _maxstorepathlen characters (or all chars of the
352 basename have been taken).
356 basename have been taken).
353 The extension (e.g. '.i' or '.d') is preserved.
357 The extension (e.g. '.i' or '.d') is preserved.
354
358
355 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
359 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
356 encoding was used.
360 encoding was used.
357 """
361 """
358 path = encodedir(path)
362 path = encodedir(path)
359 ef = _encodefname(path).split(b'/')
363 ef = _encodefname(path).split(b'/')
360 res = b'/'.join(_auxencode(ef, dotencode))
364 res = b'/'.join(_auxencode(ef, dotencode))
361 if len(res) > _maxstorepathlen:
365 if len(res) > _maxstorepathlen:
362 res = _hashencode(path, dotencode)
366 res = _hashencode(path, dotencode)
363 return res
367 return res
364
368
365
369
366 def _pathencode(path):
370 def _pathencode(path):
367 de = encodedir(path)
371 de = encodedir(path)
368 if len(path) > _maxstorepathlen:
372 if len(path) > _maxstorepathlen:
369 return _hashencode(de, True)
373 return _hashencode(de, True)
370 ef = _encodefname(de).split(b'/')
374 ef = _encodefname(de).split(b'/')
371 res = b'/'.join(_auxencode(ef, True))
375 res = b'/'.join(_auxencode(ef, True))
372 if len(res) > _maxstorepathlen:
376 if len(res) > _maxstorepathlen:
373 return _hashencode(de, True)
377 return _hashencode(de, True)
374 return res
378 return res
375
379
376
380
377 _pathencode = getattr(parsers, 'pathencode', _pathencode)
381 _pathencode = getattr(parsers, 'pathencode', _pathencode)
378
382
379
383
380 def _plainhybridencode(f):
384 def _plainhybridencode(f):
381 return _hybridencode(f, False)
385 return _hybridencode(f, False)
382
386
383
387
384 def _calcmode(vfs):
388 def _calcmode(vfs):
385 try:
389 try:
386 # files in .hg/ will be created using this mode
390 # files in .hg/ will be created using this mode
387 mode = vfs.stat().st_mode
391 mode = vfs.stat().st_mode
388 # avoid some useless chmods
392 # avoid some useless chmods
389 if (0o777 & ~util.umask) == (0o777 & mode):
393 if (0o777 & ~util.umask) == (0o777 & mode):
390 mode = None
394 mode = None
391 except OSError:
395 except OSError:
392 mode = None
396 mode = None
393 return mode
397 return mode
394
398
395
399
396 _data = [
400 _data = [
397 b'bookmarks',
401 b'bookmarks',
398 b'narrowspec',
402 b'narrowspec',
399 b'data',
403 b'data',
400 b'meta',
404 b'meta',
401 b'00manifest.d',
405 b'00manifest.d',
402 b'00manifest.i',
406 b'00manifest.i',
403 b'00changelog.d',
407 b'00changelog.d',
404 b'00changelog.i',
408 b'00changelog.i',
405 b'phaseroots',
409 b'phaseroots',
406 b'obsstore',
410 b'obsstore',
407 b'requires',
411 b'requires',
408 ]
412 ]
409
413
410 REVLOG_FILES_EXT = (
414 REVLOG_FILES_EXT = (
411 b'.i',
415 b'.i',
412 b'.idx',
416 b'.idx',
413 b'.d',
417 b'.d',
414 b'.dat',
418 b'.dat',
415 b'.n',
419 b'.n',
416 b'.nd',
420 b'.nd',
417 b'.sda',
421 b'.sda',
418 )
422 )
419 # file extension that also use a `-SOMELONGIDHASH.ext` form
423 # file extension that also use a `-SOMELONGIDHASH.ext` form
420 REVLOG_FILES_LONG_EXT = (
424 REVLOG_FILES_LONG_EXT = (
421 b'.nd',
425 b'.nd',
422 b'.idx',
426 b'.idx',
423 b'.dat',
427 b'.dat',
424 b'.sda',
428 b'.sda',
425 )
429 )
426 # files that are "volatile" and might change between listing and streaming
430 # files that are "volatile" and might change between listing and streaming
427 #
431 #
428 # note: the ".nd" file are nodemap data and won't "change" but they might be
432 # note: the ".nd" file are nodemap data and won't "change" but they might be
429 # deleted.
433 # deleted.
430 REVLOG_FILES_VOLATILE_EXT = (b'.n', b'.nd')
434 REVLOG_FILES_VOLATILE_EXT = (b'.n', b'.nd')
431
435
432 # some exception to the above matching
436 # some exception to the above matching
433 #
437 #
434 # XXX This is currently not in use because of issue6542
438 # XXX This is currently not in use because of issue6542
435 EXCLUDED = re.compile(br'.*undo\.[^/]+\.(nd?|i)$')
439 EXCLUDED = re.compile(br'.*undo\.[^/]+\.(nd?|i)$')
436
440
437
441
438 def is_revlog(f, kind, st):
442 def is_revlog(f, kind, st):
439 if kind != stat.S_IFREG:
443 if kind != stat.S_IFREG:
440 return False
444 return False
441 if f.endswith(REVLOG_FILES_EXT):
445 if f.endswith(REVLOG_FILES_EXT):
442 return True
446 return True
443 return False
447 return False
444
448
445
449
446 def is_revlog_file(f):
450 def is_revlog_file(f):
447 if f.endswith(REVLOG_FILES_EXT):
451 if f.endswith(REVLOG_FILES_EXT):
448 return True
452 return True
449 return False
453 return False
450
454
451
455
452 @attr.s(slots=True)
456 @attr.s(slots=True)
453 class StoreFile:
457 class StoreFile:
454 """a file matching a store entry"""
458 """a file matching a store entry"""
455
459
456 unencoded_path = attr.ib()
460 unencoded_path = attr.ib()
457 _file_size = attr.ib(default=None)
461 _file_size = attr.ib(default=None)
458 is_volatile = attr.ib(default=False)
462 is_volatile = attr.ib(default=False)
459
463
460 def file_size(self, vfs):
464 def file_size(self, vfs):
461 if self._file_size is None:
465 if self._file_size is None:
462 if vfs is None:
466 if vfs is None:
463 msg = b"calling vfs-less file_size without prior call: %s"
467 msg = b"calling vfs-less file_size without prior call: %s"
464 msg %= self.unencoded_path
468 msg %= self.unencoded_path
465 raise error.ProgrammingError(msg)
469 raise error.ProgrammingError(msg)
466 try:
470 try:
467 self._file_size = vfs.stat(self.unencoded_path).st_size
471 self._file_size = vfs.stat(self.unencoded_path).st_size
468 except FileNotFoundError:
472 except FileNotFoundError:
469 self._file_size = 0
473 self._file_size = 0
470 return self._file_size
474 return self._file_size
471
475
472 @property
476 @property
473 def has_size(self):
477 def has_size(self):
474 return self._file_size is not None
478 return self._file_size is not None
475
479
476 def get_stream(self, vfs, copies):
480 def get_stream(self, vfs, copies):
477 """return data "stream" information for this file
481 """return data "stream" information for this file
478
482
479 (unencoded_file_path, content_iterator, content_size)
483 (unencoded_file_path, content_iterator, content_size)
480 """
484 """
481 size = self.file_size(None)
485 size = self.file_size(None)
482
486
483 def get_stream():
487 def get_stream():
484 actual_path = copies[vfs.join(self.unencoded_path)]
488 actual_path = copies[vfs.join(self.unencoded_path)]
485 with open(actual_path, 'rb') as fp:
489 with open(actual_path, 'rb') as fp:
486 yield None # ready to stream
490 yield None # ready to stream
487 if size <= 65536:
491 if size <= 65536:
488 yield fp.read(size)
492 yield fp.read(size)
489 else:
493 else:
490 yield from util.filechunkiter(fp, limit=size)
494 yield from util.filechunkiter(fp, limit=size)
491
495
492 s = get_stream()
496 s = get_stream()
493 next(s)
497 next(s)
494 return (self.unencoded_path, s, size)
498 return (self.unencoded_path, s, size)
495
499
496
500
497 @attr.s(slots=True, init=False)
501 @attr.s(slots=True, init=False)
498 class BaseStoreEntry:
502 class BaseStoreEntry:
499 """An entry in the store
503 """An entry in the store
500
504
501 This is returned by `store.walk` and represent some data in the store."""
505 This is returned by `store.walk` and represent some data in the store."""
502
506
503 maybe_volatile = True
507 maybe_volatile = True
504
508
505 def files(self) -> List[StoreFile]:
509 def files(self) -> List[StoreFile]:
506 raise NotImplementedError
510 raise NotImplementedError
507
511
508 def get_streams(
512 def get_streams(
509 self,
513 self,
510 repo=None,
514 repo=None,
511 vfs=None,
515 vfs=None,
512 copies=None,
516 copies=None,
513 max_changeset=None,
517 max_changeset=None,
514 preserve_file_count=False,
518 preserve_file_count=False,
515 ):
519 ):
516 """return a list of data stream associated to files for this entry
520 """return a list of data stream associated to files for this entry
517
521
518 return [(unencoded_file_path, content_iterator, content_size), …]
522 return [(unencoded_file_path, content_iterator, content_size), …]
519 """
523 """
520 assert vfs is not None
524 assert vfs is not None
521 return [f.get_stream(vfs, copies) for f in self.files()]
525 return [f.get_stream(vfs, copies) for f in self.files()]
522
526
523
527
524 @attr.s(slots=True, init=False)
528 @attr.s(slots=True, init=False)
525 class SimpleStoreEntry(BaseStoreEntry):
529 class SimpleStoreEntry(BaseStoreEntry):
526 """A generic entry in the store"""
530 """A generic entry in the store"""
527
531
528 is_revlog = False
532 is_revlog = False
529
533
530 maybe_volatile = attr.ib()
534 maybe_volatile = attr.ib()
531 _entry_path = attr.ib()
535 _entry_path = attr.ib()
532 _is_volatile = attr.ib(default=False)
536 _is_volatile = attr.ib(default=False)
533 _file_size = attr.ib(default=None)
537 _file_size = attr.ib(default=None)
534 _files = attr.ib(default=None)
538 _files = attr.ib(default=None)
535
539
536 def __init__(
540 def __init__(
537 self,
541 self,
538 entry_path,
542 entry_path,
539 is_volatile=False,
543 is_volatile=False,
540 file_size=None,
544 file_size=None,
541 ):
545 ):
542 super().__init__()
546 super().__init__()
543 self._entry_path = entry_path
547 self._entry_path = entry_path
544 self._is_volatile = is_volatile
548 self._is_volatile = is_volatile
545 self._file_size = file_size
549 self._file_size = file_size
546 self._files = None
550 self._files = None
547 self.maybe_volatile = is_volatile
551 self.maybe_volatile = is_volatile
548
552
549 def files(self) -> List[StoreFile]:
553 def files(self) -> List[StoreFile]:
550 if self._files is None:
554 if self._files is None:
551 self._files = [
555 self._files = [
552 StoreFile(
556 StoreFile(
553 unencoded_path=self._entry_path,
557 unencoded_path=self._entry_path,
554 file_size=self._file_size,
558 file_size=self._file_size,
555 is_volatile=self._is_volatile,
559 is_volatile=self._is_volatile,
556 )
560 )
557 ]
561 ]
558 return self._files
562 return self._files
559
563
560
564
561 @attr.s(slots=True, init=False)
565 @attr.s(slots=True, init=False)
562 class RevlogStoreEntry(BaseStoreEntry):
566 class RevlogStoreEntry(BaseStoreEntry):
563 """A revlog entry in the store"""
567 """A revlog entry in the store"""
564
568
565 is_revlog = True
569 is_revlog = True
566
570
567 revlog_type = attr.ib(default=None)
571 revlog_type = attr.ib(default=None)
568 target_id = attr.ib(default=None)
572 target_id = attr.ib(default=None)
569 maybe_volatile = attr.ib(default=True)
573 maybe_volatile = attr.ib(default=True)
570 _path_prefix = attr.ib(default=None)
574 _path_prefix = attr.ib(default=None)
571 _details = attr.ib(default=None)
575 _details = attr.ib(default=None)
572 _files = attr.ib(default=None)
576 _files = attr.ib(default=None)
573
577
574 def __init__(
578 def __init__(
575 self,
579 self,
576 revlog_type,
580 revlog_type,
577 path_prefix,
581 path_prefix,
578 target_id,
582 target_id,
579 details,
583 details,
580 ):
584 ):
581 super().__init__()
585 super().__init__()
582 self.revlog_type = revlog_type
586 self.revlog_type = revlog_type
583 self.target_id = target_id
587 self.target_id = target_id
584 self._path_prefix = path_prefix
588 self._path_prefix = path_prefix
585 assert b'.i' in details, (path_prefix, details)
589 assert b'.i' in details, (path_prefix, details)
586 for ext in details:
590 for ext in details:
587 if ext.endswith(REVLOG_FILES_VOLATILE_EXT):
591 if ext.endswith(REVLOG_FILES_VOLATILE_EXT):
588 self.maybe_volatile = True
592 self.maybe_volatile = True
589 break
593 break
590 else:
594 else:
591 self.maybe_volatile = False
595 self.maybe_volatile = False
592 self._details = details
596 self._details = details
593 self._files = None
597 self._files = None
594
598
595 @property
599 @property
596 def is_changelog(self):
600 def is_changelog(self):
597 return self.revlog_type == KIND_CHANGELOG
601 return self.revlog_type == KIND_CHANGELOG
598
602
599 @property
603 @property
600 def is_manifestlog(self):
604 def is_manifestlog(self):
601 return self.revlog_type == KIND_MANIFESTLOG
605 return self.revlog_type == KIND_MANIFESTLOG
602
606
603 @property
607 @property
604 def is_filelog(self):
608 def is_filelog(self):
605 return self.revlog_type == KIND_FILELOG
609 return self.revlog_type == KIND_FILELOG
606
610
607 def main_file_path(self):
611 def main_file_path(self):
608 """unencoded path of the main revlog file"""
612 """unencoded path of the main revlog file"""
609 return self._path_prefix + b'.i'
613 return self._path_prefix + b'.i'
610
614
611 def files(self) -> List[StoreFile]:
615 def files(self) -> List[StoreFile]:
612 if self._files is None:
616 if self._files is None:
613 self._files = []
617 self._files = []
614 for ext in sorted(self._details, key=_ext_key):
618 for ext in sorted(self._details, key=_ext_key):
615 path = self._path_prefix + ext
619 path = self._path_prefix + ext
616 file_size = self._details[ext]
620 file_size = self._details[ext]
617 # files that are "volatile" and might change between
621 # files that are "volatile" and might change between
618 # listing and streaming
622 # listing and streaming
619 #
623 #
620 # note: the ".nd" file are nodemap data and won't "change"
624 # note: the ".nd" file are nodemap data and won't "change"
621 # but they might be deleted.
625 # but they might be deleted.
622 volatile = ext.endswith(REVLOG_FILES_VOLATILE_EXT)
626 volatile = ext.endswith(REVLOG_FILES_VOLATILE_EXT)
623 f = StoreFile(path, file_size, volatile)
627 f = StoreFile(path, file_size, volatile)
624 self._files.append(f)
628 self._files.append(f)
625 return self._files
629 return self._files
626
630
627 def get_streams(
631 def get_streams(
628 self,
632 self,
629 repo=None,
633 repo=None,
630 vfs=None,
634 vfs=None,
631 copies=None,
635 copies=None,
632 max_changeset=None,
636 max_changeset=None,
633 preserve_file_count=False,
637 preserve_file_count=False,
634 ):
638 ):
635 pre_sized = all(f.has_size for f in self.files())
639 pre_sized = all(f.has_size for f in self.files())
636 if pre_sized and (
640 if pre_sized and (
637 repo is None
641 repo is None
638 or max_changeset is None
642 or max_changeset is None
639 # This use revlog-v2, ignore for now
643 # This use revlog-v2, ignore for now
640 or any(k.endswith(b'.idx') for k in self._details.keys())
644 or any(k.endswith(b'.idx') for k in self._details.keys())
641 # This is not inline, no race expected
645 # This is not inline, no race expected
642 or b'.d' in self._details
646 or b'.d' in self._details
643 ):
647 ):
644 return super().get_streams(
648 return super().get_streams(
645 repo=repo,
649 repo=repo,
646 vfs=vfs,
650 vfs=vfs,
647 copies=copies,
651 copies=copies,
648 max_changeset=max_changeset,
652 max_changeset=max_changeset,
649 preserve_file_count=preserve_file_count,
653 preserve_file_count=preserve_file_count,
650 )
654 )
651 elif not preserve_file_count:
655 elif not preserve_file_count:
652 stream = [
656 stream = [
653 f.get_stream(vfs, copies)
657 f.get_stream(vfs, copies)
654 for f in self.files()
658 for f in self.files()
655 if not f.unencoded_path.endswith((b'.i', b'.d'))
659 if not f.unencoded_path.endswith((b'.i', b'.d'))
656 ]
660 ]
657 rl = self.get_revlog_instance(repo).get_revlog()
661 rl = self.get_revlog_instance(repo).get_revlog()
658 rl_stream = rl.get_streams(max_changeset)
662 rl_stream = rl.get_streams(max_changeset)
659 stream.extend(rl_stream)
663 stream.extend(rl_stream)
660 return stream
664 return stream
661
665
662 name_to_size = {}
666 name_to_size = {}
663 for f in self.files():
667 for f in self.files():
664 name_to_size[f.unencoded_path] = f.file_size(None)
668 name_to_size[f.unencoded_path] = f.file_size(None)
665
669
666 stream = [
670 stream = [
667 f.get_stream(vfs, copies)
671 f.get_stream(vfs, copies)
668 for f in self.files()
672 for f in self.files()
669 if not f.unencoded_path.endswith(b'.i')
673 if not f.unencoded_path.endswith(b'.i')
670 ]
674 ]
671
675
672 index_path = self._path_prefix + b'.i'
676 index_path = self._path_prefix + b'.i'
673
677
674 index_file = None
678 index_file = None
675 try:
679 try:
676 index_file = vfs(index_path)
680 index_file = vfs(index_path)
677 header = index_file.read(INDEX_HEADER.size)
681 header = index_file.read(INDEX_HEADER.size)
678 if revlogmod.revlog.is_inline_index(header):
682 if revlogmod.revlog.is_inline_index(header):
679 size = name_to_size[index_path]
683 size = name_to_size[index_path]
680
684
681 # no split underneath, just return the stream
685 # no split underneath, just return the stream
682 def get_stream():
686 def get_stream():
683 fp = index_file
687 fp = index_file
684 try:
688 try:
685 fp.seek(0)
689 fp.seek(0)
686 yield None
690 yield None
687 if size <= 65536:
691 if size <= 65536:
688 yield fp.read(size)
692 yield fp.read(size)
689 else:
693 else:
690 yield from util.filechunkiter(fp, limit=size)
694 yield from util.filechunkiter(fp, limit=size)
691 finally:
695 finally:
692 fp.close()
696 fp.close()
693
697
694 s = get_stream()
698 s = get_stream()
695 next(s)
699 next(s)
696 index_file = None
700 index_file = None
697 stream.append((index_path, s, size))
701 stream.append((index_path, s, size))
698 else:
702 else:
699 rl = self.get_revlog_instance(repo).get_revlog()
703 rl = self.get_revlog_instance(repo).get_revlog()
700 rl_stream = rl.get_streams(max_changeset, force_inline=True)
704 rl_stream = rl.get_streams(max_changeset, force_inline=True)
701 for name, s, size in rl_stream:
705 for name, s, size in rl_stream:
702 if name_to_size.get(name, 0) != size:
706 if name_to_size.get(name, 0) != size:
703 msg = _(b"expected %d bytes but %d provided for %s")
707 msg = _(b"expected %d bytes but %d provided for %s")
704 msg %= name_to_size.get(name, 0), size, name
708 msg %= name_to_size.get(name, 0), size, name
705 raise error.Abort(msg)
709 raise error.Abort(msg)
706 stream.extend(rl_stream)
710 stream.extend(rl_stream)
707 finally:
711 finally:
708 if index_file is not None:
712 if index_file is not None:
709 index_file.close()
713 index_file.close()
710
714
711 files = self.files()
715 files = self.files()
712 assert len(stream) == len(files), (
716 assert len(stream) == len(files), (
713 stream,
717 stream,
714 files,
718 files,
715 self._path_prefix,
719 self._path_prefix,
716 self.target_id,
720 self.target_id,
717 )
721 )
718 return stream
722 return stream
719
723
720 def get_revlog_instance(self, repo):
724 def get_revlog_instance(self, repo):
721 """Obtain a revlog instance from this store entry
725 """Obtain a revlog instance from this store entry
722
726
723 An instance of the appropriate class is returned.
727 An instance of the appropriate class is returned.
724 """
728 """
725 if self.is_changelog:
729 if self.is_changelog:
726 return changelog.changelog(repo.svfs)
730 return changelog.changelog(repo.svfs)
727 elif self.is_manifestlog:
731 elif self.is_manifestlog:
728 mandir = self.target_id
732 mandir = self.target_id
729 return manifest.manifestrevlog(
733 return manifest.manifestrevlog(
730 repo.nodeconstants, repo.svfs, tree=mandir
734 repo.nodeconstants, repo.svfs, tree=mandir
731 )
735 )
732 else:
736 else:
733 return filelog.filelog(repo.svfs, self.target_id)
737 return filelog.filelog(repo.svfs, self.target_id)
734
738
735
739
736 def _gather_revlog(files_data):
740 def _gather_revlog(files_data):
737 """group files per revlog prefix
741 """group files per revlog prefix
738
742
739 The returns a two level nested dict. The top level key is the revlog prefix
743 The returns a two level nested dict. The top level key is the revlog prefix
740 without extension, the second level is all the file "suffix" that were
744 without extension, the second level is all the file "suffix" that were
741 seen for this revlog and arbitrary file data as value.
745 seen for this revlog and arbitrary file data as value.
742 """
746 """
743 revlogs = collections.defaultdict(dict)
747 revlogs = collections.defaultdict(dict)
744 for u, value in files_data:
748 for u, value in files_data:
745 name, ext = _split_revlog_ext(u)
749 name, ext = _split_revlog_ext(u)
746 revlogs[name][ext] = value
750 revlogs[name][ext] = value
747 return sorted(revlogs.items())
751 return sorted(revlogs.items())
748
752
749
753
750 def _split_revlog_ext(filename):
754 def _split_revlog_ext(filename):
751 """split the revlog file prefix from the variable extension"""
755 """split the revlog file prefix from the variable extension"""
752 if filename.endswith(REVLOG_FILES_LONG_EXT):
756 if filename.endswith(REVLOG_FILES_LONG_EXT):
753 char = b'-'
757 char = b'-'
754 else:
758 else:
755 char = b'.'
759 char = b'.'
756 idx = filename.rfind(char)
760 idx = filename.rfind(char)
757 return filename[:idx], filename[idx:]
761 return filename[:idx], filename[idx:]
758
762
759
763
760 def _ext_key(ext):
764 def _ext_key(ext):
761 """a key to order revlog suffix
765 """a key to order revlog suffix
762
766
763 important to issue .i after other entry."""
767 important to issue .i after other entry."""
764 # the only important part of this order is to keep the `.i` last.
768 # the only important part of this order is to keep the `.i` last.
765 if ext.endswith(b'.n'):
769 if ext.endswith(b'.n'):
766 return (0, ext)
770 return (0, ext)
767 elif ext.endswith(b'.nd'):
771 elif ext.endswith(b'.nd'):
768 return (10, ext)
772 return (10, ext)
769 elif ext.endswith(b'.d'):
773 elif ext.endswith(b'.d'):
770 return (20, ext)
774 return (20, ext)
771 elif ext.endswith(b'.i'):
775 elif ext.endswith(b'.i'):
772 return (50, ext)
776 return (50, ext)
773 else:
777 else:
774 return (40, ext)
778 return (40, ext)
775
779
776
780
777 class basicstore:
781 class basicstore:
778 '''base class for local repository stores'''
782 '''base class for local repository stores'''
779
783
780 def __init__(self, path, vfstype):
784 def __init__(self, path, vfstype):
781 vfs = vfstype(path)
785 vfs = vfstype(path)
782 self.path = vfs.base
786 self.path = vfs.base
783 self.createmode = _calcmode(vfs)
787 self.createmode = _calcmode(vfs)
784 vfs.createmode = self.createmode
788 vfs.createmode = self.createmode
785 self.rawvfs = vfs
789 self.rawvfs = vfs
786 self.vfs = vfsmod.filtervfs(vfs, encodedir)
790 self.vfs = vfsmod.filtervfs(vfs, encodedir)
787 self.opener = self.vfs
791 self.opener = self.vfs
788
792
789 def join(self, f):
793 def join(self, f):
790 return self.path + b'/' + encodedir(f)
794 return self.path + b'/' + encodedir(f)
791
795
792 def _walk(self, relpath, recurse, undecodable=None):
796 def _walk(self, relpath, recurse, undecodable=None):
793 '''yields (revlog_type, unencoded, size)'''
797 '''yields (revlog_type, unencoded, size)'''
794 path = self.path
798 path = self.path
795 if relpath:
799 if relpath:
796 path += b'/' + relpath
800 path += b'/' + relpath
797 striplen = len(self.path) + 1
801 striplen = len(self.path) + 1
798 l = []
802 l = []
799 if self.rawvfs.isdir(path):
803 if self.rawvfs.isdir(path):
800 visit = [path]
804 visit = [path]
801 readdir = self.rawvfs.readdir
805 readdir = self.rawvfs.readdir
802 while visit:
806 while visit:
803 p = visit.pop()
807 p = visit.pop()
804 for f, kind, st in readdir(p, stat=True):
808 for f, kind, st in readdir(p, stat=True):
805 fp = p + b'/' + f
809 fp = p + b'/' + f
806 if is_revlog(f, kind, st):
810 if is_revlog(f, kind, st):
807 n = util.pconvert(fp[striplen:])
811 n = util.pconvert(fp[striplen:])
808 l.append((decodedir(n), st.st_size))
812 l.append((decodedir(n), st.st_size))
809 elif kind == stat.S_IFDIR and recurse:
813 elif kind == stat.S_IFDIR and recurse:
810 visit.append(fp)
814 visit.append(fp)
811
815
812 l.sort()
816 l.sort()
813 return l
817 return l
814
818
815 def changelog(self, trypending, concurrencychecker=None):
819 def changelog(self, trypending, concurrencychecker=None):
816 return changelog.changelog(
820 return changelog.changelog(
817 self.vfs,
821 self.vfs,
818 trypending=trypending,
822 trypending=trypending,
819 concurrencychecker=concurrencychecker,
823 concurrencychecker=concurrencychecker,
820 )
824 )
821
825
822 def manifestlog(self, repo, storenarrowmatch) -> manifest.ManifestLog:
826 def manifestlog(self, repo, storenarrowmatch) -> manifest.ManifestLog:
823 rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
827 rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
824 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
828 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
825
829
826 def data_entries(
830 def data_entries(
827 self, matcher=None, undecodable=None
831 self, matcher=None, undecodable=None
828 ) -> Generator[BaseStoreEntry, None, None]:
832 ) -> Generator[BaseStoreEntry, None, None]:
829 """Like walk, but excluding the changelog and root manifest.
833 """Like walk, but excluding the changelog and root manifest.
830
834
831 When [undecodable] is None, revlogs names that can't be
835 When [undecodable] is None, revlogs names that can't be
832 decoded cause an exception. When it is provided, it should
836 decoded cause an exception. When it is provided, it should
833 be a list and the filenames that can't be decoded are added
837 be a list and the filenames that can't be decoded are added
834 to it instead. This is very rarely needed."""
838 to it instead. This is very rarely needed."""
835 dirs = [
839 dirs = [
836 (b'data', KIND_FILELOG, False),
840 (b'data', KIND_FILELOG, False),
837 (b'meta', KIND_MANIFESTLOG, True),
841 (b'meta', KIND_MANIFESTLOG, True),
838 ]
842 ]
839 for base_dir, rl_type, strip_filename in dirs:
843 for base_dir, rl_type, strip_filename in dirs:
840 files = self._walk(base_dir, True, undecodable=undecodable)
844 files = self._walk(base_dir, True, undecodable=undecodable)
841 for revlog, details in _gather_revlog(files):
845 for revlog, details in _gather_revlog(files):
842 revlog_target_id = revlog.split(b'/', 1)[1]
846 revlog_target_id = revlog.split(b'/', 1)[1]
843 if strip_filename and b'/' in revlog:
847 if strip_filename and b'/' in revlog:
844 revlog_target_id = revlog_target_id.rsplit(b'/', 1)[0]
848 revlog_target_id = revlog_target_id.rsplit(b'/', 1)[0]
845 revlog_target_id += b'/'
849 revlog_target_id += b'/'
846 yield RevlogStoreEntry(
850 yield RevlogStoreEntry(
847 path_prefix=revlog,
851 path_prefix=revlog,
848 revlog_type=rl_type,
852 revlog_type=rl_type,
849 target_id=revlog_target_id,
853 target_id=revlog_target_id,
850 details=details,
854 details=details,
851 )
855 )
852
856
853 def top_entries(
857 def top_entries(
854 self, phase=False, obsolescence=False
858 self, phase=False, obsolescence=False
855 ) -> Generator[BaseStoreEntry, None, None]:
859 ) -> Generator[BaseStoreEntry, None, None]:
856 if phase and self.vfs.exists(b'phaseroots'):
860 if phase and self.vfs.exists(b'phaseroots'):
857 yield SimpleStoreEntry(
861 yield SimpleStoreEntry(
858 entry_path=b'phaseroots',
862 entry_path=b'phaseroots',
859 is_volatile=True,
863 is_volatile=True,
860 )
864 )
861
865
862 if obsolescence and self.vfs.exists(b'obsstore'):
866 if obsolescence and self.vfs.exists(b'obsstore'):
863 # XXX if we had the file size it could be non-volatile
867 # XXX if we had the file size it could be non-volatile
864 yield SimpleStoreEntry(
868 yield SimpleStoreEntry(
865 entry_path=b'obsstore',
869 entry_path=b'obsstore',
866 is_volatile=True,
870 is_volatile=True,
867 )
871 )
868
872
869 files = reversed(self._walk(b'', False))
873 files = reversed(self._walk(b'', False))
870
874
871 changelogs = collections.defaultdict(dict)
875 changelogs = collections.defaultdict(dict)
872 manifestlogs = collections.defaultdict(dict)
876 manifestlogs = collections.defaultdict(dict)
873
877
874 for u, s in files:
878 for u, s in files:
875 if u.startswith(b'00changelog'):
879 if u.startswith(b'00changelog'):
876 name, ext = _split_revlog_ext(u)
880 name, ext = _split_revlog_ext(u)
877 changelogs[name][ext] = s
881 changelogs[name][ext] = s
878 elif u.startswith(b'00manifest'):
882 elif u.startswith(b'00manifest'):
879 name, ext = _split_revlog_ext(u)
883 name, ext = _split_revlog_ext(u)
880 manifestlogs[name][ext] = s
884 manifestlogs[name][ext] = s
881 else:
885 else:
882 yield SimpleStoreEntry(
886 yield SimpleStoreEntry(
883 entry_path=u,
887 entry_path=u,
884 is_volatile=False,
888 is_volatile=False,
885 file_size=s,
889 file_size=s,
886 )
890 )
887 # yield manifest before changelog
891 # yield manifest before changelog
888 top_rl = [
892 top_rl = [
889 (manifestlogs, KIND_MANIFESTLOG),
893 (manifestlogs, KIND_MANIFESTLOG),
890 (changelogs, KIND_CHANGELOG),
894 (changelogs, KIND_CHANGELOG),
891 ]
895 ]
892 assert len(manifestlogs) <= 1
896 assert len(manifestlogs) <= 1
893 assert len(changelogs) <= 1
897 assert len(changelogs) <= 1
894 for data, revlog_type in top_rl:
898 for data, revlog_type in top_rl:
895 for revlog, details in sorted(data.items()):
899 for revlog, details in sorted(data.items()):
896 yield RevlogStoreEntry(
900 yield RevlogStoreEntry(
897 path_prefix=revlog,
901 path_prefix=revlog,
898 revlog_type=revlog_type,
902 revlog_type=revlog_type,
899 target_id=b'',
903 target_id=b'',
900 details=details,
904 details=details,
901 )
905 )
902
906
903 def walk(
907 def walk(
904 self, matcher=None, phase=False, obsolescence=False
908 self, matcher=None, phase=False, obsolescence=False
905 ) -> Generator[BaseStoreEntry, None, None]:
909 ) -> Generator[BaseStoreEntry, None, None]:
906 """return files related to data storage (ie: revlogs)
910 """return files related to data storage (ie: revlogs)
907
911
908 yields instance from BaseStoreEntry subclasses
912 yields instance from BaseStoreEntry subclasses
909
913
910 if a matcher is passed, storage files of only those tracked paths
914 if a matcher is passed, storage files of only those tracked paths
911 are passed with matches the matcher
915 are passed with matches the matcher
912 """
916 """
913 # yield data files first
917 # yield data files first
914 for x in self.data_entries(matcher):
918 for x in self.data_entries(matcher):
915 yield x
919 yield x
916 for x in self.top_entries(phase=phase, obsolescence=obsolescence):
920 for x in self.top_entries(phase=phase, obsolescence=obsolescence):
917 yield x
921 yield x
918
922
919 def copylist(self):
923 def copylist(self):
920 return _data
924 return _data
921
925
922 def write(self, tr):
926 def write(self, tr):
923 pass
927 pass
924
928
925 def invalidatecaches(self):
929 def invalidatecaches(self):
926 pass
930 pass
927
931
928 def markremoved(self, fn):
932 def markremoved(self, fn):
929 pass
933 pass
930
934
931 def __contains__(self, path):
935 def __contains__(self, path):
932 '''Checks if the store contains path'''
936 '''Checks if the store contains path'''
933 path = b"/".join((b"data", path))
937 path = b"/".join((b"data", path))
934 # file?
938 # file?
935 if self.vfs.exists(path + b".i"):
939 if self.vfs.exists(path + b".i"):
936 return True
940 return True
937 # dir?
941 # dir?
938 if not path.endswith(b"/"):
942 if not path.endswith(b"/"):
939 path = path + b"/"
943 path = path + b"/"
940 return self.vfs.exists(path)
944 return self.vfs.exists(path)
941
945
942
946
943 class encodedstore(basicstore):
947 class encodedstore(basicstore):
944 def __init__(self, path, vfstype):
948 def __init__(self, path, vfstype):
945 vfs = vfstype(path + b'/store')
949 vfs = vfstype(path + b'/store')
946 self.path = vfs.base
950 self.path = vfs.base
947 self.createmode = _calcmode(vfs)
951 self.createmode = _calcmode(vfs)
948 vfs.createmode = self.createmode
952 vfs.createmode = self.createmode
949 self.rawvfs = vfs
953 self.rawvfs = vfs
950 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
954 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
951 self.opener = self.vfs
955 self.opener = self.vfs
952
956
953 def _walk(self, relpath, recurse, undecodable=None):
957 def _walk(self, relpath, recurse, undecodable=None):
954 old = super()._walk(relpath, recurse)
958 old = super()._walk(relpath, recurse)
955 new = []
959 new = []
956 for f1, value in old:
960 for f1, value in old:
957 try:
961 try:
958 f2 = decodefilename(f1)
962 f2 = decodefilename(f1)
959 except KeyError:
963 except KeyError:
960 if undecodable is None:
964 if undecodable is None:
961 msg = _(b'undecodable revlog name %s') % f1
965 msg = _(b'undecodable revlog name %s') % f1
962 raise error.StorageError(msg)
966 raise error.StorageError(msg)
963 else:
967 else:
964 undecodable.append(f1)
968 undecodable.append(f1)
965 continue
969 continue
966 new.append((f2, value))
970 new.append((f2, value))
967 return new
971 return new
968
972
969 def data_entries(
973 def data_entries(
970 self, matcher=None, undecodable=None
974 self, matcher=None, undecodable=None
971 ) -> Generator[BaseStoreEntry, None, None]:
975 ) -> Generator[BaseStoreEntry, None, None]:
972 entries = super(encodedstore, self).data_entries(
976 entries = super(encodedstore, self).data_entries(
973 undecodable=undecodable
977 undecodable=undecodable
974 )
978 )
975 for entry in entries:
979 for entry in entries:
976 if _match_tracked_entry(entry, matcher):
980 if _match_tracked_entry(entry, matcher):
977 yield entry
981 yield entry
978
982
979 def join(self, f):
983 def join(self, f):
980 return self.path + b'/' + encodefilename(f)
984 return self.path + b'/' + encodefilename(f)
981
985
982 def copylist(self):
986 def copylist(self):
983 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
987 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
984
988
985
989
986 class fncache:
990 class fncache:
987 # the filename used to be partially encoded
991 # the filename used to be partially encoded
988 # hence the encodedir/decodedir dance
992 # hence the encodedir/decodedir dance
989 def __init__(self, vfs):
993 def __init__(self, vfs):
990 self.vfs = vfs
994 self.vfs = vfs
991 self._ignores = set()
995 self._ignores = set()
992 self.entries = None
996 self.entries = None
993 self._dirty = False
997 self._dirty = False
994 # set of new additions to fncache
998 # set of new additions to fncache
995 self.addls = set()
999 self.addls = set()
996
1000
997 def ensureloaded(self, warn=None):
1001 def ensureloaded(self, warn=None):
998 """read the fncache file if not already read.
1002 """read the fncache file if not already read.
999
1003
1000 If the file on disk is corrupted, raise. If warn is provided,
1004 If the file on disk is corrupted, raise. If warn is provided,
1001 warn and keep going instead."""
1005 warn and keep going instead."""
1002 if self.entries is None:
1006 if self.entries is None:
1003 self._load(warn)
1007 self._load(warn)
1004
1008
1005 def _load(self, warn=None):
1009 def _load(self, warn=None):
1006 '''fill the entries from the fncache file'''
1010 '''fill the entries from the fncache file'''
1007 self._dirty = False
1011 self._dirty = False
1008 try:
1012 try:
1009 fp = self.vfs(b'fncache', mode=b'rb')
1013 fp = self.vfs(b'fncache', mode=b'rb')
1010 except IOError:
1014 except IOError:
1011 # skip nonexistent file
1015 # skip nonexistent file
1012 self.entries = set()
1016 self.entries = set()
1013 return
1017 return
1014
1018
1015 self.entries = set()
1019 self.entries = set()
1016 chunk = b''
1020 chunk = b''
1017 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
1021 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
1018 chunk += c
1022 chunk += c
1019 try:
1023 try:
1020 p = chunk.rindex(b'\n')
1024 p = chunk.rindex(b'\n')
1021 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
1025 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
1022 chunk = chunk[p + 1 :]
1026 chunk = chunk[p + 1 :]
1023 except ValueError:
1027 except ValueError:
1024 # substring '\n' not found, maybe the entry is bigger than the
1028 # substring '\n' not found, maybe the entry is bigger than the
1025 # chunksize, so let's keep iterating
1029 # chunksize, so let's keep iterating
1026 pass
1030 pass
1027
1031
1028 if chunk:
1032 if chunk:
1029 msg = _(b"fncache does not ends with a newline")
1033 msg = _(b"fncache does not ends with a newline")
1030 if warn:
1034 if warn:
1031 warn(msg + b'\n')
1035 warn(msg + b'\n')
1032 else:
1036 else:
1033 raise error.Abort(
1037 raise error.Abort(
1034 msg,
1038 msg,
1035 hint=_(
1039 hint=_(
1036 b"use 'hg debugrebuildfncache' to "
1040 b"use 'hg debugrebuildfncache' to "
1037 b"rebuild the fncache"
1041 b"rebuild the fncache"
1038 ),
1042 ),
1039 )
1043 )
1040 self._checkentries(fp, warn)
1044 self._checkentries(fp, warn)
1041 fp.close()
1045 fp.close()
1042
1046
1043 def _checkentries(self, fp, warn):
1047 def _checkentries(self, fp, warn):
1044 """make sure there is no empty string in entries"""
1048 """make sure there is no empty string in entries"""
1045 if b'' in self.entries:
1049 if b'' in self.entries:
1046 fp.seek(0)
1050 fp.seek(0)
1047 for n, line in enumerate(fp):
1051 for n, line in enumerate(fp):
1048 if not line.rstrip(b'\n'):
1052 if not line.rstrip(b'\n'):
1049 t = _(b'invalid entry in fncache, line %d') % (n + 1)
1053 t = _(b'invalid entry in fncache, line %d') % (n + 1)
1050 if warn:
1054 if warn:
1051 warn(t + b'\n')
1055 warn(t + b'\n')
1052 else:
1056 else:
1053 raise error.Abort(t)
1057 raise error.Abort(t)
1054
1058
1055 def write(self, tr):
1059 def write(self, tr):
1056 if self._dirty:
1060 if self._dirty:
1057 assert self.entries is not None
1061 assert self.entries is not None
1058 self.entries = self.entries | self.addls
1062 self.entries = self.entries | self.addls
1059 self.addls = set()
1063 self.addls = set()
1060 tr.addbackup(b'fncache')
1064 tr.addbackup(b'fncache')
1061 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
1065 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
1062 if self.entries:
1066 if self.entries:
1063 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
1067 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
1064 fp.close()
1068 fp.close()
1065 self._dirty = False
1069 self._dirty = False
1066 if self.addls:
1070 if self.addls:
1067 # if we have just new entries, let's append them to the fncache
1071 # if we have just new entries, let's append them to the fncache
1068 tr.addbackup(b'fncache')
1072 tr.addbackup(b'fncache')
1069 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
1073 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
1070 if self.addls:
1074 if self.addls:
1071 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
1075 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
1072 fp.close()
1076 fp.close()
1073 self.entries = None
1077 self.entries = None
1074 self.addls = set()
1078 self.addls = set()
1075
1079
1076 def addignore(self, fn):
1080 def addignore(self, fn):
1077 self._ignores.add(fn)
1081 self._ignores.add(fn)
1078
1082
1079 def add(self, fn):
1083 def add(self, fn):
1080 if fn in self._ignores:
1084 if fn in self._ignores:
1081 return
1085 return
1082 if self.entries is None:
1086 if self.entries is None:
1083 self._load()
1087 self._load()
1084 if fn not in self.entries:
1088 if fn not in self.entries:
1085 self.addls.add(fn)
1089 self.addls.add(fn)
1086
1090
1087 def remove(self, fn):
1091 def remove(self, fn):
1088 if self.entries is None:
1092 if self.entries is None:
1089 self._load()
1093 self._load()
1090 if fn in self.addls:
1094 if fn in self.addls:
1091 self.addls.remove(fn)
1095 self.addls.remove(fn)
1092 return
1096 return
1093 try:
1097 try:
1094 self.entries.remove(fn)
1098 self.entries.remove(fn)
1095 self._dirty = True
1099 self._dirty = True
1096 except KeyError:
1100 except KeyError:
1097 pass
1101 pass
1098
1102
1099 def __contains__(self, fn):
1103 def __contains__(self, fn):
1100 if fn in self.addls:
1104 if fn in self.addls:
1101 return True
1105 return True
1102 if self.entries is None:
1106 if self.entries is None:
1103 self._load()
1107 self._load()
1104 return fn in self.entries
1108 return fn in self.entries
1105
1109
1106 def __iter__(self):
1110 def __iter__(self):
1107 if self.entries is None:
1111 if self.entries is None:
1108 self._load()
1112 self._load()
1109 return iter(self.entries | self.addls)
1113 return iter(self.entries | self.addls)
1110
1114
1111
1115
1112 class _fncachevfs(vfsmod.proxyvfs):
1116 class _fncachevfs(vfsmod.proxyvfs):
1113 def __init__(self, vfs, fnc, encode):
1117 def __init__(self, vfs, fnc, encode):
1114 vfsmod.proxyvfs.__init__(self, vfs)
1118 vfsmod.proxyvfs.__init__(self, vfs)
1115 self.fncache = fnc
1119 self.fncache = fnc
1116 self.encode = encode
1120 self.encode = encode
1117
1121
1118 def __call__(self, path, mode=b'r', *args, **kw):
1122 def __call__(self, path, mode=b'r', *args, **kw):
1119 encoded = self.encode(path)
1123 encoded = self.encode(path)
1120 if (
1124 if (
1121 mode not in (b'r', b'rb')
1125 mode not in (b'r', b'rb')
1122 and (path.startswith(b'data/') or path.startswith(b'meta/'))
1126 and (path.startswith(b'data/') or path.startswith(b'meta/'))
1123 and is_revlog_file(path)
1127 and is_revlog_file(path)
1124 ):
1128 ):
1125 # do not trigger a fncache load when adding a file that already is
1129 # do not trigger a fncache load when adding a file that already is
1126 # known to exist.
1130 # known to exist.
1127 notload = self.fncache.entries is None and (
1131 notload = self.fncache.entries is None and (
1128 # if the file has size zero, it should be considered as missing.
1132 # if the file has size zero, it should be considered as missing.
1129 # Such zero-size files are the result of truncation when a
1133 # Such zero-size files are the result of truncation when a
1130 # transaction is aborted.
1134 # transaction is aborted.
1131 self.vfs.exists(encoded)
1135 self.vfs.exists(encoded)
1132 and self.vfs.stat(encoded).st_size
1136 and self.vfs.stat(encoded).st_size
1133 )
1137 )
1134 if not notload:
1138 if not notload:
1135 self.fncache.add(path)
1139 self.fncache.add(path)
1136 return self.vfs(encoded, mode, *args, **kw)
1140 return self.vfs(encoded, mode, *args, **kw)
1137
1141
1138 def join(self, path):
1142 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
1143 insidef = (self.encode(f) for f in insidef)
1144
1139 if path:
1145 if path:
1140 return self.vfs.join(self.encode(path))
1146 return self.vfs.join(self.encode(path), *insidef)
1141 else:
1147 else:
1142 return self.vfs.join(path)
1148 return self.vfs.join(path, *insidef)
1143
1149
1144 def register_file(self, path):
1150 def register_file(self, path):
1145 """generic hook point to lets fncache steer its stew"""
1151 """generic hook point to lets fncache steer its stew"""
1146 if path.startswith(b'data/') or path.startswith(b'meta/'):
1152 if path.startswith(b'data/') or path.startswith(b'meta/'):
1147 self.fncache.add(path)
1153 self.fncache.add(path)
1148
1154
1149
1155
1150 class fncachestore(basicstore):
1156 class fncachestore(basicstore):
1151 def __init__(self, path, vfstype, dotencode):
1157 def __init__(self, path, vfstype, dotencode):
1152 if dotencode:
1158 if dotencode:
1153 encode = _pathencode
1159 encode = _pathencode
1154 else:
1160 else:
1155 encode = _plainhybridencode
1161 encode = _plainhybridencode
1156 self.encode = encode
1162 self.encode = encode
1157 vfs = vfstype(path + b'/store')
1163 vfs = vfstype(path + b'/store')
1158 self.path = vfs.base
1164 self.path = vfs.base
1159 self.pathsep = self.path + b'/'
1165 self.pathsep = self.path + b'/'
1160 self.createmode = _calcmode(vfs)
1166 self.createmode = _calcmode(vfs)
1161 vfs.createmode = self.createmode
1167 vfs.createmode = self.createmode
1162 self.rawvfs = vfs
1168 self.rawvfs = vfs
1163 fnc = fncache(vfs)
1169 fnc = fncache(vfs)
1164 self.fncache = fnc
1170 self.fncache = fnc
1165 self.vfs = _fncachevfs(vfs, fnc, encode)
1171 self.vfs = _fncachevfs(vfs, fnc, encode)
1166 self.opener = self.vfs
1172 self.opener = self.vfs
1167
1173
1168 def join(self, f):
1174 def join(self, f):
1169 return self.pathsep + self.encode(f)
1175 return self.pathsep + self.encode(f)
1170
1176
1171 def getsize(self, path):
1177 def getsize(self, path):
1172 return self.rawvfs.stat(path).st_size
1178 return self.rawvfs.stat(path).st_size
1173
1179
1174 def data_entries(
1180 def data_entries(
1175 self, matcher=None, undecodable=None
1181 self, matcher=None, undecodable=None
1176 ) -> Generator[BaseStoreEntry, None, None]:
1182 ) -> Generator[BaseStoreEntry, None, None]:
1177 # Note: all files in fncache should be revlog related, However the
1183 # Note: all files in fncache should be revlog related, However the
1178 # fncache might contains such file added by previous version of
1184 # fncache might contains such file added by previous version of
1179 # Mercurial.
1185 # Mercurial.
1180 files = ((f, None) for f in self.fncache if is_revlog_file(f))
1186 files = ((f, None) for f in self.fncache if is_revlog_file(f))
1181 by_revlog = _gather_revlog(files)
1187 by_revlog = _gather_revlog(files)
1182 for revlog, details in by_revlog:
1188 for revlog, details in by_revlog:
1183 if revlog.startswith(b'data/'):
1189 if revlog.startswith(b'data/'):
1184 rl_type = KIND_FILELOG
1190 rl_type = KIND_FILELOG
1185 revlog_target_id = revlog.split(b'/', 1)[1]
1191 revlog_target_id = revlog.split(b'/', 1)[1]
1186 elif revlog.startswith(b'meta/'):
1192 elif revlog.startswith(b'meta/'):
1187 rl_type = KIND_MANIFESTLOG
1193 rl_type = KIND_MANIFESTLOG
1188 # drop the initial directory and the `00manifest` file part
1194 # drop the initial directory and the `00manifest` file part
1189 tmp = revlog.split(b'/', 1)[1]
1195 tmp = revlog.split(b'/', 1)[1]
1190 revlog_target_id = tmp.rsplit(b'/', 1)[0] + b'/'
1196 revlog_target_id = tmp.rsplit(b'/', 1)[0] + b'/'
1191 else:
1197 else:
1192 # unreachable
1198 # unreachable
1193 assert False, revlog
1199 assert False, revlog
1194 entry = RevlogStoreEntry(
1200 entry = RevlogStoreEntry(
1195 path_prefix=revlog,
1201 path_prefix=revlog,
1196 revlog_type=rl_type,
1202 revlog_type=rl_type,
1197 target_id=revlog_target_id,
1203 target_id=revlog_target_id,
1198 details=details,
1204 details=details,
1199 )
1205 )
1200 if _match_tracked_entry(entry, matcher):
1206 if _match_tracked_entry(entry, matcher):
1201 yield entry
1207 yield entry
1202
1208
1203 def copylist(self):
1209 def copylist(self):
1204 d = (
1210 d = (
1205 b'bookmarks',
1211 b'bookmarks',
1206 b'narrowspec',
1212 b'narrowspec',
1207 b'data',
1213 b'data',
1208 b'meta',
1214 b'meta',
1209 b'dh',
1215 b'dh',
1210 b'fncache',
1216 b'fncache',
1211 b'phaseroots',
1217 b'phaseroots',
1212 b'obsstore',
1218 b'obsstore',
1213 b'00manifest.d',
1219 b'00manifest.d',
1214 b'00manifest.i',
1220 b'00manifest.i',
1215 b'00changelog.d',
1221 b'00changelog.d',
1216 b'00changelog.i',
1222 b'00changelog.i',
1217 b'requires',
1223 b'requires',
1218 )
1224 )
1219 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in d]
1225 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in d]
1220
1226
1221 def write(self, tr):
1227 def write(self, tr):
1222 self.fncache.write(tr)
1228 self.fncache.write(tr)
1223
1229
1224 def invalidatecaches(self):
1230 def invalidatecaches(self):
1225 self.fncache.entries = None
1231 self.fncache.entries = None
1226 self.fncache.addls = set()
1232 self.fncache.addls = set()
1227
1233
1228 def markremoved(self, fn):
1234 def markremoved(self, fn):
1229 self.fncache.remove(fn)
1235 self.fncache.remove(fn)
1230
1236
1231 def _exists(self, f):
1237 def _exists(self, f):
1232 ef = self.encode(f)
1238 ef = self.encode(f)
1233 try:
1239 try:
1234 self.getsize(ef)
1240 self.getsize(ef)
1235 return True
1241 return True
1236 except FileNotFoundError:
1242 except FileNotFoundError:
1237 return False
1243 return False
1238
1244
1239 def __contains__(self, path):
1245 def __contains__(self, path):
1240 '''Checks if the store contains path'''
1246 '''Checks if the store contains path'''
1241 path = b"/".join((b"data", path))
1247 path = b"/".join((b"data", path))
1242 # check for files (exact match)
1248 # check for files (exact match)
1243 e = path + b'.i'
1249 e = path + b'.i'
1244 if e in self.fncache and self._exists(e):
1250 if e in self.fncache and self._exists(e):
1245 return True
1251 return True
1246 # now check for directories (prefix match)
1252 # now check for directories (prefix match)
1247 if not path.endswith(b'/'):
1253 if not path.endswith(b'/'):
1248 path += b'/'
1254 path += b'/'
1249 for e in self.fncache:
1255 for e in self.fncache:
1250 if e.startswith(path) and self._exists(e):
1256 if e.startswith(path) and self._exists(e):
1251 return True
1257 return True
1252 return False
1258 return False
General Comments 0
You need to be logged in to leave comments. Login now