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