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