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