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