##// END OF EJS Templates
py3: iterate bytes as a byte string in store.lowerencode()
Yuya Nishihara -
r34212:b4abc438 default
parent child Browse files
Show More
@@ -1,578 +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 xchr = pycompat.bytechr
160 xchr = pycompat.bytechr
161 cmap = dict([(xchr(x), xchr(x)) for x in xrange(127)])
161 cmap = dict([(xchr(x), xchr(x)) for x in xrange(127)])
162 for x in _reserved():
162 for x in _reserved():
163 cmap[xchr(x)] = "~%02x" % x
163 cmap[xchr(x)] = "~%02x" % x
164 for x in range(ord("A"), ord("Z") + 1):
164 for x in range(ord("A"), ord("Z") + 1):
165 cmap[xchr(x)] = xchr(x).lower()
165 cmap[xchr(x)] = xchr(x).lower()
166 def lowerencode(s):
166 def lowerencode(s):
167 return "".join([cmap[c] for c in s])
167 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
168 return lowerencode
168 return lowerencode
169
169
170 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
170 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
171
171
172 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
172 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
173 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
173 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
174 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
174 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
175 def _auxencode(path, dotencode):
175 def _auxencode(path, dotencode):
176 '''
176 '''
177 Encodes filenames containing names reserved by Windows or which end in
177 Encodes filenames containing names reserved by Windows or which end in
178 period or space. Does not touch other single reserved characters c.
178 period or space. Does not touch other single reserved characters c.
179 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
179 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
180 Additionally encodes space or period at the beginning, if dotencode is
180 Additionally encodes space or period at the beginning, if dotencode is
181 True. Parameter path is assumed to be all lowercase.
181 True. Parameter path is assumed to be all lowercase.
182 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
183 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"
184 doesn't need encoding.
184 doesn't need encoding.
185
185
186 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
186 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
187 >>> _auxencode(s.split(b'/'), True)
187 >>> _auxencode(s.split(b'/'), True)
188 ['~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']
189 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
189 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
190 >>> _auxencode(s.split(b'/'), False)
190 >>> _auxencode(s.split(b'/'), False)
191 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
191 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
192 >>> _auxencode([b'foo. '], True)
192 >>> _auxencode([b'foo. '], True)
193 ['foo.~20']
193 ['foo.~20']
194 >>> _auxencode([b' .foo'], True)
194 >>> _auxencode([b' .foo'], True)
195 ['~20.foo']
195 ['~20.foo']
196 '''
196 '''
197 for i, n in enumerate(path):
197 for i, n in enumerate(path):
198 if not n:
198 if not n:
199 continue
199 continue
200 if dotencode and n[0] in '. ':
200 if dotencode and n[0] in '. ':
201 n = "~%02x" % ord(n[0:1]) + n[1:]
201 n = "~%02x" % ord(n[0:1]) + n[1:]
202 path[i] = n
202 path[i] = n
203 else:
203 else:
204 l = n.find('.')
204 l = n.find('.')
205 if l == -1:
205 if l == -1:
206 l = len(n)
206 l = len(n)
207 if ((l == 3 and n[:3] in _winres3) or
207 if ((l == 3 and n[:3] in _winres3) or
208 (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'
209 and n[:3] in _winres4)):
209 and n[:3] in _winres4)):
210 # encode third letter ('aux' -> 'au~78')
210 # encode third letter ('aux' -> 'au~78')
211 ec = "~%02x" % ord(n[2:3])
211 ec = "~%02x" % ord(n[2:3])
212 n = n[0:2] + ec + n[3:]
212 n = n[0:2] + ec + n[3:]
213 path[i] = n
213 path[i] = n
214 if n[-1] in '. ':
214 if n[-1] in '. ':
215 # encode last period or space ('foo...' -> 'foo..~2e')
215 # encode last period or space ('foo...' -> 'foo..~2e')
216 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
216 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
217 return path
217 return path
218
218
219 _maxstorepathlen = 120
219 _maxstorepathlen = 120
220 _dirprefixlen = 8
220 _dirprefixlen = 8
221 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
221 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
222
222
223 def _hashencode(path, dotencode):
223 def _hashencode(path, dotencode):
224 digest = hashlib.sha1(path).hexdigest()
224 digest = hashlib.sha1(path).hexdigest()
225 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
225 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
226 parts = _auxencode(le, dotencode)
226 parts = _auxencode(le, dotencode)
227 basename = parts[-1]
227 basename = parts[-1]
228 _root, ext = os.path.splitext(basename)
228 _root, ext = os.path.splitext(basename)
229 sdirs = []
229 sdirs = []
230 sdirslen = 0
230 sdirslen = 0
231 for p in parts[:-1]:
231 for p in parts[:-1]:
232 d = p[:_dirprefixlen]
232 d = p[:_dirprefixlen]
233 if d[-1] in '. ':
233 if d[-1] in '. ':
234 # Windows can't access dirs ending in period or space
234 # Windows can't access dirs ending in period or space
235 d = d[:-1] + '_'
235 d = d[:-1] + '_'
236 if sdirslen == 0:
236 if sdirslen == 0:
237 t = len(d)
237 t = len(d)
238 else:
238 else:
239 t = sdirslen + 1 + len(d)
239 t = sdirslen + 1 + len(d)
240 if t > _maxshortdirslen:
240 if t > _maxshortdirslen:
241 break
241 break
242 sdirs.append(d)
242 sdirs.append(d)
243 sdirslen = t
243 sdirslen = t
244 dirs = '/'.join(sdirs)
244 dirs = '/'.join(sdirs)
245 if len(dirs) > 0:
245 if len(dirs) > 0:
246 dirs += '/'
246 dirs += '/'
247 res = 'dh/' + dirs + digest + ext
247 res = 'dh/' + dirs + digest + ext
248 spaceleft = _maxstorepathlen - len(res)
248 spaceleft = _maxstorepathlen - len(res)
249 if spaceleft > 0:
249 if spaceleft > 0:
250 filler = basename[:spaceleft]
250 filler = basename[:spaceleft]
251 res = 'dh/' + dirs + filler + digest + ext
251 res = 'dh/' + dirs + filler + digest + ext
252 return res
252 return res
253
253
254 def _hybridencode(path, dotencode):
254 def _hybridencode(path, dotencode):
255 '''encodes path with a length limit
255 '''encodes path with a length limit
256
256
257 Encodes all paths that begin with 'data/', according to the following.
257 Encodes all paths that begin with 'data/', according to the following.
258
258
259 Default encoding (reversible):
259 Default encoding (reversible):
260
260
261 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
261 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
262 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
263 of the character (see encodefilename).
263 of the character (see encodefilename).
264 Relevant path components consisting of Windows reserved filenames are
264 Relevant path components consisting of Windows reserved filenames are
265 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
265 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
266
266
267 Hashed encoding (not reversible):
267 Hashed encoding (not reversible):
268
268
269 If the default-encoded path is longer than _maxstorepathlen, a
269 If the default-encoded path is longer than _maxstorepathlen, a
270 non-reversible hybrid hashing of the path is done instead.
270 non-reversible hybrid hashing of the path is done instead.
271 This encoding uses up to _dirprefixlen characters of all directory
271 This encoding uses up to _dirprefixlen characters of all directory
272 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
273 _maxshortdirslen.
273 _maxshortdirslen.
274 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.
275 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
276 (the basename is everything after the last path separator). The filler
276 (the basename is everything after the last path separator). The filler
277 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
278 the encoded path has _maxstorepathlen characters (or all chars of the
278 the encoded path has _maxstorepathlen characters (or all chars of the
279 basename have been taken).
279 basename have been taken).
280 The extension (e.g. '.i' or '.d') is preserved.
280 The extension (e.g. '.i' or '.d') is preserved.
281
281
282 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
283 encoding was used.
283 encoding was used.
284 '''
284 '''
285 path = encodedir(path)
285 path = encodedir(path)
286 ef = _encodefname(path).split('/')
286 ef = _encodefname(path).split('/')
287 res = '/'.join(_auxencode(ef, dotencode))
287 res = '/'.join(_auxencode(ef, dotencode))
288 if len(res) > _maxstorepathlen:
288 if len(res) > _maxstorepathlen:
289 res = _hashencode(path, dotencode)
289 res = _hashencode(path, dotencode)
290 return res
290 return res
291
291
292 def _pathencode(path):
292 def _pathencode(path):
293 de = encodedir(path)
293 de = encodedir(path)
294 if len(path) > _maxstorepathlen:
294 if len(path) > _maxstorepathlen:
295 return _hashencode(de, True)
295 return _hashencode(de, True)
296 ef = _encodefname(de).split('/')
296 ef = _encodefname(de).split('/')
297 res = '/'.join(_auxencode(ef, True))
297 res = '/'.join(_auxencode(ef, True))
298 if len(res) > _maxstorepathlen:
298 if len(res) > _maxstorepathlen:
299 return _hashencode(de, True)
299 return _hashencode(de, True)
300 return res
300 return res
301
301
302 _pathencode = getattr(parsers, 'pathencode', _pathencode)
302 _pathencode = getattr(parsers, 'pathencode', _pathencode)
303
303
304 def _plainhybridencode(f):
304 def _plainhybridencode(f):
305 return _hybridencode(f, False)
305 return _hybridencode(f, False)
306
306
307 def _calcmode(vfs):
307 def _calcmode(vfs):
308 try:
308 try:
309 # files in .hg/ will be created using this mode
309 # files in .hg/ will be created using this mode
310 mode = vfs.stat().st_mode
310 mode = vfs.stat().st_mode
311 # avoid some useless chmods
311 # avoid some useless chmods
312 if (0o777 & ~util.umask) == (0o777 & mode):
312 if (0o777 & ~util.umask) == (0o777 & mode):
313 mode = None
313 mode = None
314 except OSError:
314 except OSError:
315 mode = None
315 mode = None
316 return mode
316 return mode
317
317
318 _data = ('data meta 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
318 _data = ('data meta 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
319 ' phaseroots obsstore')
319 ' phaseroots obsstore')
320
320
321 class basicstore(object):
321 class basicstore(object):
322 '''base class for local repository stores'''
322 '''base class for local repository stores'''
323 def __init__(self, path, vfstype):
323 def __init__(self, path, vfstype):
324 vfs = vfstype(path)
324 vfs = vfstype(path)
325 self.path = vfs.base
325 self.path = vfs.base
326 self.createmode = _calcmode(vfs)
326 self.createmode = _calcmode(vfs)
327 vfs.createmode = self.createmode
327 vfs.createmode = self.createmode
328 self.rawvfs = vfs
328 self.rawvfs = vfs
329 self.vfs = vfsmod.filtervfs(vfs, encodedir)
329 self.vfs = vfsmod.filtervfs(vfs, encodedir)
330 self.opener = self.vfs
330 self.opener = self.vfs
331
331
332 def join(self, f):
332 def join(self, f):
333 return self.path + '/' + encodedir(f)
333 return self.path + '/' + encodedir(f)
334
334
335 def _walk(self, relpath, recurse):
335 def _walk(self, relpath, recurse):
336 '''yields (unencoded, encoded, size)'''
336 '''yields (unencoded, encoded, size)'''
337 path = self.path
337 path = self.path
338 if relpath:
338 if relpath:
339 path += '/' + relpath
339 path += '/' + relpath
340 striplen = len(self.path) + 1
340 striplen = len(self.path) + 1
341 l = []
341 l = []
342 if self.rawvfs.isdir(path):
342 if self.rawvfs.isdir(path):
343 visit = [path]
343 visit = [path]
344 readdir = self.rawvfs.readdir
344 readdir = self.rawvfs.readdir
345 while visit:
345 while visit:
346 p = visit.pop()
346 p = visit.pop()
347 for f, kind, st in readdir(p, stat=True):
347 for f, kind, st in readdir(p, stat=True):
348 fp = p + '/' + f
348 fp = p + '/' + f
349 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
349 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
350 n = util.pconvert(fp[striplen:])
350 n = util.pconvert(fp[striplen:])
351 l.append((decodedir(n), n, st.st_size))
351 l.append((decodedir(n), n, st.st_size))
352 elif kind == stat.S_IFDIR and recurse:
352 elif kind == stat.S_IFDIR and recurse:
353 visit.append(fp)
353 visit.append(fp)
354 l.sort()
354 l.sort()
355 return l
355 return l
356
356
357 def datafiles(self):
357 def datafiles(self):
358 return self._walk('data', True) + self._walk('meta', True)
358 return self._walk('data', True) + self._walk('meta', True)
359
359
360 def topfiles(self):
360 def topfiles(self):
361 # yield manifest before changelog
361 # yield manifest before changelog
362 return reversed(self._walk('', False))
362 return reversed(self._walk('', False))
363
363
364 def walk(self):
364 def walk(self):
365 '''yields (unencoded, encoded, size)'''
365 '''yields (unencoded, encoded, size)'''
366 # yield data files first
366 # yield data files first
367 for x in self.datafiles():
367 for x in self.datafiles():
368 yield x
368 yield x
369 for x in self.topfiles():
369 for x in self.topfiles():
370 yield x
370 yield x
371
371
372 def copylist(self):
372 def copylist(self):
373 return ['requires'] + _data.split()
373 return ['requires'] + _data.split()
374
374
375 def write(self, tr):
375 def write(self, tr):
376 pass
376 pass
377
377
378 def invalidatecaches(self):
378 def invalidatecaches(self):
379 pass
379 pass
380
380
381 def markremoved(self, fn):
381 def markremoved(self, fn):
382 pass
382 pass
383
383
384 def __contains__(self, path):
384 def __contains__(self, path):
385 '''Checks if the store contains path'''
385 '''Checks if the store contains path'''
386 path = "/".join(("data", path))
386 path = "/".join(("data", path))
387 # file?
387 # file?
388 if self.vfs.exists(path + ".i"):
388 if self.vfs.exists(path + ".i"):
389 return True
389 return True
390 # dir?
390 # dir?
391 if not path.endswith("/"):
391 if not path.endswith("/"):
392 path = path + "/"
392 path = path + "/"
393 return self.vfs.exists(path)
393 return self.vfs.exists(path)
394
394
395 class encodedstore(basicstore):
395 class encodedstore(basicstore):
396 def __init__(self, path, vfstype):
396 def __init__(self, path, vfstype):
397 vfs = vfstype(path + '/store')
397 vfs = vfstype(path + '/store')
398 self.path = vfs.base
398 self.path = vfs.base
399 self.createmode = _calcmode(vfs)
399 self.createmode = _calcmode(vfs)
400 vfs.createmode = self.createmode
400 vfs.createmode = self.createmode
401 self.rawvfs = vfs
401 self.rawvfs = vfs
402 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
402 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
403 self.opener = self.vfs
403 self.opener = self.vfs
404
404
405 def datafiles(self):
405 def datafiles(self):
406 for a, b, size in super(encodedstore, self).datafiles():
406 for a, b, size in super(encodedstore, self).datafiles():
407 try:
407 try:
408 a = decodefilename(a)
408 a = decodefilename(a)
409 except KeyError:
409 except KeyError:
410 a = None
410 a = None
411 yield a, b, size
411 yield a, b, size
412
412
413 def join(self, f):
413 def join(self, f):
414 return self.path + '/' + encodefilename(f)
414 return self.path + '/' + encodefilename(f)
415
415
416 def copylist(self):
416 def copylist(self):
417 return (['requires', '00changelog.i'] +
417 return (['requires', '00changelog.i'] +
418 ['store/' + f for f in _data.split()])
418 ['store/' + f for f in _data.split()])
419
419
420 class fncache(object):
420 class fncache(object):
421 # the filename used to be partially encoded
421 # the filename used to be partially encoded
422 # hence the encodedir/decodedir dance
422 # hence the encodedir/decodedir dance
423 def __init__(self, vfs):
423 def __init__(self, vfs):
424 self.vfs = vfs
424 self.vfs = vfs
425 self.entries = None
425 self.entries = None
426 self._dirty = False
426 self._dirty = False
427
427
428 def _load(self):
428 def _load(self):
429 '''fill the entries from the fncache file'''
429 '''fill the entries from the fncache file'''
430 self._dirty = False
430 self._dirty = False
431 try:
431 try:
432 fp = self.vfs('fncache', mode='rb')
432 fp = self.vfs('fncache', mode='rb')
433 except IOError:
433 except IOError:
434 # skip nonexistent file
434 # skip nonexistent file
435 self.entries = set()
435 self.entries = set()
436 return
436 return
437 self.entries = set(decodedir(fp.read()).splitlines())
437 self.entries = set(decodedir(fp.read()).splitlines())
438 if '' in self.entries:
438 if '' in self.entries:
439 fp.seek(0)
439 fp.seek(0)
440 for n, line in enumerate(util.iterfile(fp)):
440 for n, line in enumerate(util.iterfile(fp)):
441 if not line.rstrip('\n'):
441 if not line.rstrip('\n'):
442 t = _('invalid entry in fncache, line %d') % (n + 1)
442 t = _('invalid entry in fncache, line %d') % (n + 1)
443 raise error.Abort(t)
443 raise error.Abort(t)
444 fp.close()
444 fp.close()
445
445
446 def write(self, tr):
446 def write(self, tr):
447 if self._dirty:
447 if self._dirty:
448 tr.addbackup('fncache')
448 tr.addbackup('fncache')
449 fp = self.vfs('fncache', mode='wb', atomictemp=True)
449 fp = self.vfs('fncache', mode='wb', atomictemp=True)
450 if self.entries:
450 if self.entries:
451 fp.write(encodedir('\n'.join(self.entries) + '\n'))
451 fp.write(encodedir('\n'.join(self.entries) + '\n'))
452 fp.close()
452 fp.close()
453 self._dirty = False
453 self._dirty = False
454
454
455 def add(self, fn):
455 def add(self, fn):
456 if self.entries is None:
456 if self.entries is None:
457 self._load()
457 self._load()
458 if fn not in self.entries:
458 if fn not in self.entries:
459 self._dirty = True
459 self._dirty = True
460 self.entries.add(fn)
460 self.entries.add(fn)
461
461
462 def remove(self, fn):
462 def remove(self, fn):
463 if self.entries is None:
463 if self.entries is None:
464 self._load()
464 self._load()
465 try:
465 try:
466 self.entries.remove(fn)
466 self.entries.remove(fn)
467 self._dirty = True
467 self._dirty = True
468 except KeyError:
468 except KeyError:
469 pass
469 pass
470
470
471 def __contains__(self, fn):
471 def __contains__(self, fn):
472 if self.entries is None:
472 if self.entries is None:
473 self._load()
473 self._load()
474 return fn in self.entries
474 return fn in self.entries
475
475
476 def __iter__(self):
476 def __iter__(self):
477 if self.entries is None:
477 if self.entries is None:
478 self._load()
478 self._load()
479 return iter(self.entries)
479 return iter(self.entries)
480
480
481 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
481 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
482 def __init__(self, vfs, fnc, encode):
482 def __init__(self, vfs, fnc, encode):
483 vfsmod.proxyvfs.__init__(self, vfs)
483 vfsmod.proxyvfs.__init__(self, vfs)
484 self.fncache = fnc
484 self.fncache = fnc
485 self.encode = encode
485 self.encode = encode
486
486
487 def __call__(self, path, mode='r', *args, **kw):
487 def __call__(self, path, mode='r', *args, **kw):
488 if mode not in ('r', 'rb') and (path.startswith('data/') or
488 if mode not in ('r', 'rb') and (path.startswith('data/') or
489 path.startswith('meta/')):
489 path.startswith('meta/')):
490 self.fncache.add(path)
490 self.fncache.add(path)
491 return self.vfs(self.encode(path), mode, *args, **kw)
491 return self.vfs(self.encode(path), mode, *args, **kw)
492
492
493 def join(self, path):
493 def join(self, path):
494 if path:
494 if path:
495 return self.vfs.join(self.encode(path))
495 return self.vfs.join(self.encode(path))
496 else:
496 else:
497 return self.vfs.join(path)
497 return self.vfs.join(path)
498
498
499 class fncachestore(basicstore):
499 class fncachestore(basicstore):
500 def __init__(self, path, vfstype, dotencode):
500 def __init__(self, path, vfstype, dotencode):
501 if dotencode:
501 if dotencode:
502 encode = _pathencode
502 encode = _pathencode
503 else:
503 else:
504 encode = _plainhybridencode
504 encode = _plainhybridencode
505 self.encode = encode
505 self.encode = encode
506 vfs = vfstype(path + '/store')
506 vfs = vfstype(path + '/store')
507 self.path = vfs.base
507 self.path = vfs.base
508 self.pathsep = self.path + '/'
508 self.pathsep = self.path + '/'
509 self.createmode = _calcmode(vfs)
509 self.createmode = _calcmode(vfs)
510 vfs.createmode = self.createmode
510 vfs.createmode = self.createmode
511 self.rawvfs = vfs
511 self.rawvfs = vfs
512 fnc = fncache(vfs)
512 fnc = fncache(vfs)
513 self.fncache = fnc
513 self.fncache = fnc
514 self.vfs = _fncachevfs(vfs, fnc, encode)
514 self.vfs = _fncachevfs(vfs, fnc, encode)
515 self.opener = self.vfs
515 self.opener = self.vfs
516
516
517 def join(self, f):
517 def join(self, f):
518 return self.pathsep + self.encode(f)
518 return self.pathsep + self.encode(f)
519
519
520 def getsize(self, path):
520 def getsize(self, path):
521 return self.rawvfs.stat(path).st_size
521 return self.rawvfs.stat(path).st_size
522
522
523 def datafiles(self):
523 def datafiles(self):
524 for f in sorted(self.fncache):
524 for f in sorted(self.fncache):
525 ef = self.encode(f)
525 ef = self.encode(f)
526 try:
526 try:
527 yield f, ef, self.getsize(ef)
527 yield f, ef, self.getsize(ef)
528 except OSError as err:
528 except OSError as err:
529 if err.errno != errno.ENOENT:
529 if err.errno != errno.ENOENT:
530 raise
530 raise
531
531
532 def copylist(self):
532 def copylist(self):
533 d = ('data meta dh fncache phaseroots obsstore'
533 d = ('data meta dh fncache phaseroots obsstore'
534 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
534 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
535 return (['requires', '00changelog.i'] +
535 return (['requires', '00changelog.i'] +
536 ['store/' + f for f in d.split()])
536 ['store/' + f for f in d.split()])
537
537
538 def write(self, tr):
538 def write(self, tr):
539 self.fncache.write(tr)
539 self.fncache.write(tr)
540
540
541 def invalidatecaches(self):
541 def invalidatecaches(self):
542 self.fncache.entries = None
542 self.fncache.entries = None
543
543
544 def markremoved(self, fn):
544 def markremoved(self, fn):
545 self.fncache.remove(fn)
545 self.fncache.remove(fn)
546
546
547 def _exists(self, f):
547 def _exists(self, f):
548 ef = self.encode(f)
548 ef = self.encode(f)
549 try:
549 try:
550 self.getsize(ef)
550 self.getsize(ef)
551 return True
551 return True
552 except OSError as err:
552 except OSError as err:
553 if err.errno != errno.ENOENT:
553 if err.errno != errno.ENOENT:
554 raise
554 raise
555 # nonexistent entry
555 # nonexistent entry
556 return False
556 return False
557
557
558 def __contains__(self, path):
558 def __contains__(self, path):
559 '''Checks if the store contains path'''
559 '''Checks if the store contains path'''
560 path = "/".join(("data", path))
560 path = "/".join(("data", path))
561 # check for files (exact match)
561 # check for files (exact match)
562 e = path + '.i'
562 e = path + '.i'
563 if e in self.fncache and self._exists(e):
563 if e in self.fncache and self._exists(e):
564 return True
564 return True
565 # now check for directories (prefix match)
565 # now check for directories (prefix match)
566 if not path.endswith('/'):
566 if not path.endswith('/'):
567 path += '/'
567 path += '/'
568 for e in self.fncache:
568 for e in self.fncache:
569 if e.startswith(path) and self._exists(e):
569 if e.startswith(path) and self._exists(e):
570 return True
570 return True
571 return False
571 return False
572
572
573 def store(requirements, path, vfstype):
573 def store(requirements, path, vfstype):
574 if 'store' in requirements:
574 if 'store' in requirements:
575 if 'fncache' in requirements:
575 if 'fncache' in requirements:
576 return fncachestore(path, vfstype, 'dotencode' in requirements)
576 return fncachestore(path, vfstype, 'dotencode' in requirements)
577 return encodedstore(path, vfstype)
577 return encodedstore(path, vfstype)
578 return basicstore(path, vfstype)
578 return basicstore(path, vfstype)
@@ -1,81 +1,81 b''
1 # this is hack to make sure no escape characters are inserted into the output
1 # this is hack to make sure no escape characters are inserted into the output
2
2
3 from __future__ import absolute_import
3 from __future__ import absolute_import
4
4
5 import doctest
5 import doctest
6 import os
6 import os
7 import re
7 import re
8 import sys
8 import sys
9
9
10 ispy3 = (sys.version_info[0] >= 3)
10 ispy3 = (sys.version_info[0] >= 3)
11
11
12 if 'TERM' in os.environ:
12 if 'TERM' in os.environ:
13 del os.environ['TERM']
13 del os.environ['TERM']
14
14
15 class py3docchecker(doctest.OutputChecker):
15 class py3docchecker(doctest.OutputChecker):
16 def check_output(self, want, got, optionflags):
16 def check_output(self, want, got, optionflags):
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
20 # <exc.name>: <others> -> <name>: <others>
20 # <exc.name>: <others> -> <name>: <others>
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
22 got2, re.MULTILINE)
22 got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
25 for w, g in [(want, got), (want2, got2)])
25 for w, g in [(want, got), (want2, got2)])
26
26
27 # TODO: migrate doctests to py3 and enable them on both versions
27 # TODO: migrate doctests to py3 and enable them on both versions
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
29 if not (not ispy3 and py2 or ispy3 and py3):
29 if not (not ispy3 and py2 or ispy3 and py3):
30 return
30 return
31 __import__(name)
31 __import__(name)
32 mod = sys.modules[name]
32 mod = sys.modules[name]
33 if testtarget is not None:
33 if testtarget is not None:
34 mod = getattr(mod, testtarget)
34 mod = getattr(mod, testtarget)
35
35
36 # minimal copy of doctest.testmod()
36 # minimal copy of doctest.testmod()
37 finder = doctest.DocTestFinder()
37 finder = doctest.DocTestFinder()
38 checker = None
38 checker = None
39 if ispy3:
39 if ispy3:
40 checker = py3docchecker()
40 checker = py3docchecker()
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
42 for test in finder.find(mod, name):
42 for test in finder.find(mod, name):
43 runner.run(test)
43 runner.run(test)
44 runner.summarize()
44 runner.summarize()
45
45
46 testmod('mercurial.changegroup')
46 testmod('mercurial.changegroup')
47 testmod('mercurial.changelog')
47 testmod('mercurial.changelog')
48 testmod('mercurial.color')
48 testmod('mercurial.color')
49 testmod('mercurial.config')
49 testmod('mercurial.config')
50 testmod('mercurial.context')
50 testmod('mercurial.context')
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
52 testmod('mercurial.dispatch')
52 testmod('mercurial.dispatch')
53 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
53 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
54 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
54 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
55 testmod('mercurial.hg')
55 testmod('mercurial.hg')
56 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
56 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
57 testmod('mercurial.match')
57 testmod('mercurial.match')
58 testmod('mercurial.mdiff')
58 testmod('mercurial.mdiff')
59 testmod('mercurial.minirst')
59 testmod('mercurial.minirst')
60 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
60 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
61 testmod('mercurial.pathutil', py3=False) # py3: os.sep
61 testmod('mercurial.pathutil', py3=False) # py3: os.sep
62 testmod('mercurial.parser')
62 testmod('mercurial.parser')
63 testmod('mercurial.pycompat')
63 testmod('mercurial.pycompat')
64 testmod('mercurial.revsetlang')
64 testmod('mercurial.revsetlang')
65 testmod('mercurial.smartset')
65 testmod('mercurial.smartset')
66 testmod('mercurial.store', py3=False) # py3: bytes[n]
66 testmod('mercurial.store')
67 testmod('mercurial.subrepo')
67 testmod('mercurial.subrepo')
68 testmod('mercurial.templatefilters')
68 testmod('mercurial.templatefilters')
69 testmod('mercurial.templater')
69 testmod('mercurial.templater')
70 testmod('mercurial.ui')
70 testmod('mercurial.ui')
71 testmod('mercurial.url')
71 testmod('mercurial.url')
72 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
72 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
73 testmod('mercurial.util', testtarget='platform')
73 testmod('mercurial.util', testtarget='platform')
74 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
74 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
75 testmod('hgext.convert.cvsps')
75 testmod('hgext.convert.cvsps')
76 testmod('hgext.convert.filemap')
76 testmod('hgext.convert.filemap')
77 testmod('hgext.convert.p4')
77 testmod('hgext.convert.p4')
78 testmod('hgext.convert.subversion')
78 testmod('hgext.convert.subversion')
79 testmod('hgext.mq')
79 testmod('hgext.mq')
80 # Helper scripts in tests/ that have doctests:
80 # Helper scripts in tests/ that have doctests:
81 testmod('drawdag')
81 testmod('drawdag')
General Comments 0
You need to be logged in to leave comments. Login now