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