##// END OF EJS Templates
py3: use bytechr() in store._buildlowerencodefun()
Yuya Nishihara -
r34211:7e3f078b default
parent child Browse files
Show More
@@ -1,577 +1,578 b''
1 # store.py - repository store handling for Mercurial
1 # store.py - repository store handling for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import 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 xchr = pycompat.bytechr
161 cmap = dict([(xchr(x), xchr(x)) for x in xrange(127)])
161 for x in _reserved():
162 for x in _reserved():
162 cmap[chr(x)] = "~%02x" % x
163 cmap[xchr(x)] = "~%02x" % x
163 for x in range(ord("A"), ord("Z") + 1):
164 for x in range(ord("A"), ord("Z") + 1):
164 cmap[chr(x)] = chr(x).lower()
165 cmap[xchr(x)] = xchr(x).lower()
165 def lowerencode(s):
166 def lowerencode(s):
166 return "".join([cmap[c] for c in s])
167 return "".join([cmap[c] for c in s])
167 return lowerencode
168 return lowerencode
168
169
169 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
170 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
170
171
171 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
172 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
172 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
173 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
173 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
174 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
174 def _auxencode(path, dotencode):
175 def _auxencode(path, dotencode):
175 '''
176 '''
176 Encodes filenames containing names reserved by Windows or which end in
177 Encodes filenames containing names reserved by Windows or which end in
177 period or space. Does not touch other single reserved characters c.
178 period or space. Does not touch other single reserved characters c.
178 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
179 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
179 Additionally encodes space or period at the beginning, if dotencode is
180 Additionally encodes space or period at the beginning, if dotencode is
180 True. Parameter path is assumed to be all lowercase.
181 True. Parameter path is assumed to be all lowercase.
181 A segment only needs encoding if a reserved name appears as a
182 A segment only needs encoding if a reserved name appears as a
182 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
183 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
183 doesn't need encoding.
184 doesn't need encoding.
184
185
185 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
186 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
186 >>> _auxencode(s.split(b'/'), True)
187 >>> _auxencode(s.split(b'/'), True)
187 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
188 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
188 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
189 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
189 >>> _auxencode(s.split(b'/'), False)
190 >>> _auxencode(s.split(b'/'), False)
190 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
191 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
191 >>> _auxencode([b'foo. '], True)
192 >>> _auxencode([b'foo. '], True)
192 ['foo.~20']
193 ['foo.~20']
193 >>> _auxencode([b' .foo'], True)
194 >>> _auxencode([b' .foo'], True)
194 ['~20.foo']
195 ['~20.foo']
195 '''
196 '''
196 for i, n in enumerate(path):
197 for i, n in enumerate(path):
197 if not n:
198 if not n:
198 continue
199 continue
199 if dotencode and n[0] in '. ':
200 if dotencode and n[0] in '. ':
200 n = "~%02x" % ord(n[0:1]) + n[1:]
201 n = "~%02x" % ord(n[0:1]) + n[1:]
201 path[i] = n
202 path[i] = n
202 else:
203 else:
203 l = n.find('.')
204 l = n.find('.')
204 if l == -1:
205 if l == -1:
205 l = len(n)
206 l = len(n)
206 if ((l == 3 and n[:3] in _winres3) or
207 if ((l == 3 and n[:3] in _winres3) or
207 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
208 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
208 and n[:3] in _winres4)):
209 and n[:3] in _winres4)):
209 # encode third letter ('aux' -> 'au~78')
210 # encode third letter ('aux' -> 'au~78')
210 ec = "~%02x" % ord(n[2:3])
211 ec = "~%02x" % ord(n[2:3])
211 n = n[0:2] + ec + n[3:]
212 n = n[0:2] + ec + n[3:]
212 path[i] = n
213 path[i] = n
213 if n[-1] in '. ':
214 if n[-1] in '. ':
214 # encode last period or space ('foo...' -> 'foo..~2e')
215 # encode last period or space ('foo...' -> 'foo..~2e')
215 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
216 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
216 return path
217 return path
217
218
218 _maxstorepathlen = 120
219 _maxstorepathlen = 120
219 _dirprefixlen = 8
220 _dirprefixlen = 8
220 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
221 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
221
222
222 def _hashencode(path, dotencode):
223 def _hashencode(path, dotencode):
223 digest = hashlib.sha1(path).hexdigest()
224 digest = hashlib.sha1(path).hexdigest()
224 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
225 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
225 parts = _auxencode(le, dotencode)
226 parts = _auxencode(le, dotencode)
226 basename = parts[-1]
227 basename = parts[-1]
227 _root, ext = os.path.splitext(basename)
228 _root, ext = os.path.splitext(basename)
228 sdirs = []
229 sdirs = []
229 sdirslen = 0
230 sdirslen = 0
230 for p in parts[:-1]:
231 for p in parts[:-1]:
231 d = p[:_dirprefixlen]
232 d = p[:_dirprefixlen]
232 if d[-1] in '. ':
233 if d[-1] in '. ':
233 # Windows can't access dirs ending in period or space
234 # Windows can't access dirs ending in period or space
234 d = d[:-1] + '_'
235 d = d[:-1] + '_'
235 if sdirslen == 0:
236 if sdirslen == 0:
236 t = len(d)
237 t = len(d)
237 else:
238 else:
238 t = sdirslen + 1 + len(d)
239 t = sdirslen + 1 + len(d)
239 if t > _maxshortdirslen:
240 if t > _maxshortdirslen:
240 break
241 break
241 sdirs.append(d)
242 sdirs.append(d)
242 sdirslen = t
243 sdirslen = t
243 dirs = '/'.join(sdirs)
244 dirs = '/'.join(sdirs)
244 if len(dirs) > 0:
245 if len(dirs) > 0:
245 dirs += '/'
246 dirs += '/'
246 res = 'dh/' + dirs + digest + ext
247 res = 'dh/' + dirs + digest + ext
247 spaceleft = _maxstorepathlen - len(res)
248 spaceleft = _maxstorepathlen - len(res)
248 if spaceleft > 0:
249 if spaceleft > 0:
249 filler = basename[:spaceleft]
250 filler = basename[:spaceleft]
250 res = 'dh/' + dirs + filler + digest + ext
251 res = 'dh/' + dirs + filler + digest + ext
251 return res
252 return res
252
253
253 def _hybridencode(path, dotencode):
254 def _hybridencode(path, dotencode):
254 '''encodes path with a length limit
255 '''encodes path with a length limit
255
256
256 Encodes all paths that begin with 'data/', according to the following.
257 Encodes all paths that begin with 'data/', according to the following.
257
258
258 Default encoding (reversible):
259 Default encoding (reversible):
259
260
260 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
261 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
261 characters are encoded as '~xx', where xx is the two digit hex code
262 characters are encoded as '~xx', where xx is the two digit hex code
262 of the character (see encodefilename).
263 of the character (see encodefilename).
263 Relevant path components consisting of Windows reserved filenames are
264 Relevant path components consisting of Windows reserved filenames are
264 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
265 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
265
266
266 Hashed encoding (not reversible):
267 Hashed encoding (not reversible):
267
268
268 If the default-encoded path is longer than _maxstorepathlen, a
269 If the default-encoded path is longer than _maxstorepathlen, a
269 non-reversible hybrid hashing of the path is done instead.
270 non-reversible hybrid hashing of the path is done instead.
270 This encoding uses up to _dirprefixlen characters of all directory
271 This encoding uses up to _dirprefixlen characters of all directory
271 levels of the lowerencoded path, but not more levels than can fit into
272 levels of the lowerencoded path, but not more levels than can fit into
272 _maxshortdirslen.
273 _maxshortdirslen.
273 Then follows the filler followed by the sha digest of the full path.
274 Then follows the filler followed by the sha digest of the full path.
274 The filler is the beginning of the basename of the lowerencoded path
275 The filler is the beginning of the basename of the lowerencoded path
275 (the basename is everything after the last path separator). The filler
276 (the basename is everything after the last path separator). The filler
276 is as long as possible, filling in characters from the basename until
277 is as long as possible, filling in characters from the basename until
277 the encoded path has _maxstorepathlen characters (or all chars of the
278 the encoded path has _maxstorepathlen characters (or all chars of the
278 basename have been taken).
279 basename have been taken).
279 The extension (e.g. '.i' or '.d') is preserved.
280 The extension (e.g. '.i' or '.d') is preserved.
280
281
281 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
282 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
282 encoding was used.
283 encoding was used.
283 '''
284 '''
284 path = encodedir(path)
285 path = encodedir(path)
285 ef = _encodefname(path).split('/')
286 ef = _encodefname(path).split('/')
286 res = '/'.join(_auxencode(ef, dotencode))
287 res = '/'.join(_auxencode(ef, dotencode))
287 if len(res) > _maxstorepathlen:
288 if len(res) > _maxstorepathlen:
288 res = _hashencode(path, dotencode)
289 res = _hashencode(path, dotencode)
289 return res
290 return res
290
291
291 def _pathencode(path):
292 def _pathencode(path):
292 de = encodedir(path)
293 de = encodedir(path)
293 if len(path) > _maxstorepathlen:
294 if len(path) > _maxstorepathlen:
294 return _hashencode(de, True)
295 return _hashencode(de, True)
295 ef = _encodefname(de).split('/')
296 ef = _encodefname(de).split('/')
296 res = '/'.join(_auxencode(ef, True))
297 res = '/'.join(_auxencode(ef, True))
297 if len(res) > _maxstorepathlen:
298 if len(res) > _maxstorepathlen:
298 return _hashencode(de, True)
299 return _hashencode(de, True)
299 return res
300 return res
300
301
301 _pathencode = getattr(parsers, 'pathencode', _pathencode)
302 _pathencode = getattr(parsers, 'pathencode', _pathencode)
302
303
303 def _plainhybridencode(f):
304 def _plainhybridencode(f):
304 return _hybridencode(f, False)
305 return _hybridencode(f, False)
305
306
306 def _calcmode(vfs):
307 def _calcmode(vfs):
307 try:
308 try:
308 # files in .hg/ will be created using this mode
309 # files in .hg/ will be created using this mode
309 mode = vfs.stat().st_mode
310 mode = vfs.stat().st_mode
310 # avoid some useless chmods
311 # avoid some useless chmods
311 if (0o777 & ~util.umask) == (0o777 & mode):
312 if (0o777 & ~util.umask) == (0o777 & mode):
312 mode = None
313 mode = None
313 except OSError:
314 except OSError:
314 mode = None
315 mode = None
315 return mode
316 return mode
316
317
317 _data = ('data meta 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
318 _data = ('data meta 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
318 ' phaseroots obsstore')
319 ' phaseroots obsstore')
319
320
320 class basicstore(object):
321 class basicstore(object):
321 '''base class for local repository stores'''
322 '''base class for local repository stores'''
322 def __init__(self, path, vfstype):
323 def __init__(self, path, vfstype):
323 vfs = vfstype(path)
324 vfs = vfstype(path)
324 self.path = vfs.base
325 self.path = vfs.base
325 self.createmode = _calcmode(vfs)
326 self.createmode = _calcmode(vfs)
326 vfs.createmode = self.createmode
327 vfs.createmode = self.createmode
327 self.rawvfs = vfs
328 self.rawvfs = vfs
328 self.vfs = vfsmod.filtervfs(vfs, encodedir)
329 self.vfs = vfsmod.filtervfs(vfs, encodedir)
329 self.opener = self.vfs
330 self.opener = self.vfs
330
331
331 def join(self, f):
332 def join(self, f):
332 return self.path + '/' + encodedir(f)
333 return self.path + '/' + encodedir(f)
333
334
334 def _walk(self, relpath, recurse):
335 def _walk(self, relpath, recurse):
335 '''yields (unencoded, encoded, size)'''
336 '''yields (unencoded, encoded, size)'''
336 path = self.path
337 path = self.path
337 if relpath:
338 if relpath:
338 path += '/' + relpath
339 path += '/' + relpath
339 striplen = len(self.path) + 1
340 striplen = len(self.path) + 1
340 l = []
341 l = []
341 if self.rawvfs.isdir(path):
342 if self.rawvfs.isdir(path):
342 visit = [path]
343 visit = [path]
343 readdir = self.rawvfs.readdir
344 readdir = self.rawvfs.readdir
344 while visit:
345 while visit:
345 p = visit.pop()
346 p = visit.pop()
346 for f, kind, st in readdir(p, stat=True):
347 for f, kind, st in readdir(p, stat=True):
347 fp = p + '/' + f
348 fp = p + '/' + f
348 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
349 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
349 n = util.pconvert(fp[striplen:])
350 n = util.pconvert(fp[striplen:])
350 l.append((decodedir(n), n, st.st_size))
351 l.append((decodedir(n), n, st.st_size))
351 elif kind == stat.S_IFDIR and recurse:
352 elif kind == stat.S_IFDIR and recurse:
352 visit.append(fp)
353 visit.append(fp)
353 l.sort()
354 l.sort()
354 return l
355 return l
355
356
356 def datafiles(self):
357 def datafiles(self):
357 return self._walk('data', True) + self._walk('meta', True)
358 return self._walk('data', True) + self._walk('meta', True)
358
359
359 def topfiles(self):
360 def topfiles(self):
360 # yield manifest before changelog
361 # yield manifest before changelog
361 return reversed(self._walk('', False))
362 return reversed(self._walk('', False))
362
363
363 def walk(self):
364 def walk(self):
364 '''yields (unencoded, encoded, size)'''
365 '''yields (unencoded, encoded, size)'''
365 # yield data files first
366 # yield data files first
366 for x in self.datafiles():
367 for x in self.datafiles():
367 yield x
368 yield x
368 for x in self.topfiles():
369 for x in self.topfiles():
369 yield x
370 yield x
370
371
371 def copylist(self):
372 def copylist(self):
372 return ['requires'] + _data.split()
373 return ['requires'] + _data.split()
373
374
374 def write(self, tr):
375 def write(self, tr):
375 pass
376 pass
376
377
377 def invalidatecaches(self):
378 def invalidatecaches(self):
378 pass
379 pass
379
380
380 def markremoved(self, fn):
381 def markremoved(self, fn):
381 pass
382 pass
382
383
383 def __contains__(self, path):
384 def __contains__(self, path):
384 '''Checks if the store contains path'''
385 '''Checks if the store contains path'''
385 path = "/".join(("data", path))
386 path = "/".join(("data", path))
386 # file?
387 # file?
387 if self.vfs.exists(path + ".i"):
388 if self.vfs.exists(path + ".i"):
388 return True
389 return True
389 # dir?
390 # dir?
390 if not path.endswith("/"):
391 if not path.endswith("/"):
391 path = path + "/"
392 path = path + "/"
392 return self.vfs.exists(path)
393 return self.vfs.exists(path)
393
394
394 class encodedstore(basicstore):
395 class encodedstore(basicstore):
395 def __init__(self, path, vfstype):
396 def __init__(self, path, vfstype):
396 vfs = vfstype(path + '/store')
397 vfs = vfstype(path + '/store')
397 self.path = vfs.base
398 self.path = vfs.base
398 self.createmode = _calcmode(vfs)
399 self.createmode = _calcmode(vfs)
399 vfs.createmode = self.createmode
400 vfs.createmode = self.createmode
400 self.rawvfs = vfs
401 self.rawvfs = vfs
401 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
402 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
402 self.opener = self.vfs
403 self.opener = self.vfs
403
404
404 def datafiles(self):
405 def datafiles(self):
405 for a, b, size in super(encodedstore, self).datafiles():
406 for a, b, size in super(encodedstore, self).datafiles():
406 try:
407 try:
407 a = decodefilename(a)
408 a = decodefilename(a)
408 except KeyError:
409 except KeyError:
409 a = None
410 a = None
410 yield a, b, size
411 yield a, b, size
411
412
412 def join(self, f):
413 def join(self, f):
413 return self.path + '/' + encodefilename(f)
414 return self.path + '/' + encodefilename(f)
414
415
415 def copylist(self):
416 def copylist(self):
416 return (['requires', '00changelog.i'] +
417 return (['requires', '00changelog.i'] +
417 ['store/' + f for f in _data.split()])
418 ['store/' + f for f in _data.split()])
418
419
419 class fncache(object):
420 class fncache(object):
420 # the filename used to be partially encoded
421 # the filename used to be partially encoded
421 # hence the encodedir/decodedir dance
422 # hence the encodedir/decodedir dance
422 def __init__(self, vfs):
423 def __init__(self, vfs):
423 self.vfs = vfs
424 self.vfs = vfs
424 self.entries = None
425 self.entries = None
425 self._dirty = False
426 self._dirty = False
426
427
427 def _load(self):
428 def _load(self):
428 '''fill the entries from the fncache file'''
429 '''fill the entries from the fncache file'''
429 self._dirty = False
430 self._dirty = False
430 try:
431 try:
431 fp = self.vfs('fncache', mode='rb')
432 fp = self.vfs('fncache', mode='rb')
432 except IOError:
433 except IOError:
433 # skip nonexistent file
434 # skip nonexistent file
434 self.entries = set()
435 self.entries = set()
435 return
436 return
436 self.entries = set(decodedir(fp.read()).splitlines())
437 self.entries = set(decodedir(fp.read()).splitlines())
437 if '' in self.entries:
438 if '' in self.entries:
438 fp.seek(0)
439 fp.seek(0)
439 for n, line in enumerate(util.iterfile(fp)):
440 for n, line in enumerate(util.iterfile(fp)):
440 if not line.rstrip('\n'):
441 if not line.rstrip('\n'):
441 t = _('invalid entry in fncache, line %d') % (n + 1)
442 t = _('invalid entry in fncache, line %d') % (n + 1)
442 raise error.Abort(t)
443 raise error.Abort(t)
443 fp.close()
444 fp.close()
444
445
445 def write(self, tr):
446 def write(self, tr):
446 if self._dirty:
447 if self._dirty:
447 tr.addbackup('fncache')
448 tr.addbackup('fncache')
448 fp = self.vfs('fncache', mode='wb', atomictemp=True)
449 fp = self.vfs('fncache', mode='wb', atomictemp=True)
449 if self.entries:
450 if self.entries:
450 fp.write(encodedir('\n'.join(self.entries) + '\n'))
451 fp.write(encodedir('\n'.join(self.entries) + '\n'))
451 fp.close()
452 fp.close()
452 self._dirty = False
453 self._dirty = False
453
454
454 def add(self, fn):
455 def add(self, fn):
455 if self.entries is None:
456 if self.entries is None:
456 self._load()
457 self._load()
457 if fn not in self.entries:
458 if fn not in self.entries:
458 self._dirty = True
459 self._dirty = True
459 self.entries.add(fn)
460 self.entries.add(fn)
460
461
461 def remove(self, fn):
462 def remove(self, fn):
462 if self.entries is None:
463 if self.entries is None:
463 self._load()
464 self._load()
464 try:
465 try:
465 self.entries.remove(fn)
466 self.entries.remove(fn)
466 self._dirty = True
467 self._dirty = True
467 except KeyError:
468 except KeyError:
468 pass
469 pass
469
470
470 def __contains__(self, fn):
471 def __contains__(self, fn):
471 if self.entries is None:
472 if self.entries is None:
472 self._load()
473 self._load()
473 return fn in self.entries
474 return fn in self.entries
474
475
475 def __iter__(self):
476 def __iter__(self):
476 if self.entries is None:
477 if self.entries is None:
477 self._load()
478 self._load()
478 return iter(self.entries)
479 return iter(self.entries)
479
480
480 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
481 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
481 def __init__(self, vfs, fnc, encode):
482 def __init__(self, vfs, fnc, encode):
482 vfsmod.proxyvfs.__init__(self, vfs)
483 vfsmod.proxyvfs.__init__(self, vfs)
483 self.fncache = fnc
484 self.fncache = fnc
484 self.encode = encode
485 self.encode = encode
485
486
486 def __call__(self, path, mode='r', *args, **kw):
487 def __call__(self, path, mode='r', *args, **kw):
487 if mode not in ('r', 'rb') and (path.startswith('data/') or
488 if mode not in ('r', 'rb') and (path.startswith('data/') or
488 path.startswith('meta/')):
489 path.startswith('meta/')):
489 self.fncache.add(path)
490 self.fncache.add(path)
490 return self.vfs(self.encode(path), mode, *args, **kw)
491 return self.vfs(self.encode(path), mode, *args, **kw)
491
492
492 def join(self, path):
493 def join(self, path):
493 if path:
494 if path:
494 return self.vfs.join(self.encode(path))
495 return self.vfs.join(self.encode(path))
495 else:
496 else:
496 return self.vfs.join(path)
497 return self.vfs.join(path)
497
498
498 class fncachestore(basicstore):
499 class fncachestore(basicstore):
499 def __init__(self, path, vfstype, dotencode):
500 def __init__(self, path, vfstype, dotencode):
500 if dotencode:
501 if dotencode:
501 encode = _pathencode
502 encode = _pathencode
502 else:
503 else:
503 encode = _plainhybridencode
504 encode = _plainhybridencode
504 self.encode = encode
505 self.encode = encode
505 vfs = vfstype(path + '/store')
506 vfs = vfstype(path + '/store')
506 self.path = vfs.base
507 self.path = vfs.base
507 self.pathsep = self.path + '/'
508 self.pathsep = self.path + '/'
508 self.createmode = _calcmode(vfs)
509 self.createmode = _calcmode(vfs)
509 vfs.createmode = self.createmode
510 vfs.createmode = self.createmode
510 self.rawvfs = vfs
511 self.rawvfs = vfs
511 fnc = fncache(vfs)
512 fnc = fncache(vfs)
512 self.fncache = fnc
513 self.fncache = fnc
513 self.vfs = _fncachevfs(vfs, fnc, encode)
514 self.vfs = _fncachevfs(vfs, fnc, encode)
514 self.opener = self.vfs
515 self.opener = self.vfs
515
516
516 def join(self, f):
517 def join(self, f):
517 return self.pathsep + self.encode(f)
518 return self.pathsep + self.encode(f)
518
519
519 def getsize(self, path):
520 def getsize(self, path):
520 return self.rawvfs.stat(path).st_size
521 return self.rawvfs.stat(path).st_size
521
522
522 def datafiles(self):
523 def datafiles(self):
523 for f in sorted(self.fncache):
524 for f in sorted(self.fncache):
524 ef = self.encode(f)
525 ef = self.encode(f)
525 try:
526 try:
526 yield f, ef, self.getsize(ef)
527 yield f, ef, self.getsize(ef)
527 except OSError as err:
528 except OSError as err:
528 if err.errno != errno.ENOENT:
529 if err.errno != errno.ENOENT:
529 raise
530 raise
530
531
531 def copylist(self):
532 def copylist(self):
532 d = ('data meta dh fncache phaseroots obsstore'
533 d = ('data meta dh fncache phaseroots obsstore'
533 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
534 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
534 return (['requires', '00changelog.i'] +
535 return (['requires', '00changelog.i'] +
535 ['store/' + f for f in d.split()])
536 ['store/' + f for f in d.split()])
536
537
537 def write(self, tr):
538 def write(self, tr):
538 self.fncache.write(tr)
539 self.fncache.write(tr)
539
540
540 def invalidatecaches(self):
541 def invalidatecaches(self):
541 self.fncache.entries = None
542 self.fncache.entries = None
542
543
543 def markremoved(self, fn):
544 def markremoved(self, fn):
544 self.fncache.remove(fn)
545 self.fncache.remove(fn)
545
546
546 def _exists(self, f):
547 def _exists(self, f):
547 ef = self.encode(f)
548 ef = self.encode(f)
548 try:
549 try:
549 self.getsize(ef)
550 self.getsize(ef)
550 return True
551 return True
551 except OSError as err:
552 except OSError as err:
552 if err.errno != errno.ENOENT:
553 if err.errno != errno.ENOENT:
553 raise
554 raise
554 # nonexistent entry
555 # nonexistent entry
555 return False
556 return False
556
557
557 def __contains__(self, path):
558 def __contains__(self, path):
558 '''Checks if the store contains path'''
559 '''Checks if the store contains path'''
559 path = "/".join(("data", path))
560 path = "/".join(("data", path))
560 # check for files (exact match)
561 # check for files (exact match)
561 e = path + '.i'
562 e = path + '.i'
562 if e in self.fncache and self._exists(e):
563 if e in self.fncache and self._exists(e):
563 return True
564 return True
564 # now check for directories (prefix match)
565 # now check for directories (prefix match)
565 if not path.endswith('/'):
566 if not path.endswith('/'):
566 path += '/'
567 path += '/'
567 for e in self.fncache:
568 for e in self.fncache:
568 if e.startswith(path) and self._exists(e):
569 if e.startswith(path) and self._exists(e):
569 return True
570 return True
570 return False
571 return False
571
572
572 def store(requirements, path, vfstype):
573 def store(requirements, path, vfstype):
573 if 'store' in requirements:
574 if 'store' in requirements:
574 if 'fncache' in requirements:
575 if 'fncache' in requirements:
575 return fncachestore(path, vfstype, 'dotencode' in requirements)
576 return fncachestore(path, vfstype, 'dotencode' in requirements)
576 return encodedstore(path, vfstype)
577 return encodedstore(path, vfstype)
577 return basicstore(path, vfstype)
578 return basicstore(path, vfstype)
General Comments 0
You need to be logged in to leave comments. Login now