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