##// END OF EJS Templates
store: initialize "vfs" fields by "vfs" constructors...
FUJIWARA Katsunori -
r17653:dacb5069 default
parent child Browse files
Show More
@@ -1,494 +1,497 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 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 ef = _encodefname(encodedir(path)).split('/')
259 259 res = '/'.join(_auxencode(ef, True))
260 260 if len(res) > _maxstorepathlen:
261 261 return None
262 262 return res
263 263
264 264 _pathencode = getattr(parsers, 'pathencode', _pathencode)
265 265
266 266 def _dothybridencode(f):
267 267 ef = _pathencode(f)
268 268 if ef is None:
269 269 return _hashencode(encodedir(f), True)
270 270 return ef
271 271
272 272 def _plainhybridencode(f):
273 273 return _hybridencode(f, False)
274 274
275 275 def _calcmode(path):
276 276 try:
277 277 # files in .hg/ will be created using this mode
278 278 mode = os.stat(path).st_mode
279 279 # avoid some useless chmods
280 280 if (0777 & ~util.umask) == (0777 & mode):
281 281 mode = None
282 282 except OSError:
283 283 mode = None
284 284 return mode
285 285
286 286 _data = ('data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
287 287 ' phaseroots obsstore')
288 288
289 289 class basicstore(object):
290 290 '''base class for local repository stores'''
291 291 def __init__(self, path, vfstype):
292 292 self.path = path
293 293 self.createmode = _calcmode(path)
294 294 vfs = vfstype(self.path)
295 295 vfs.createmode = self.createmode
296 self.opener = scmutil.filteropener(vfs, encodedir)
296 self.vfs = scmutil.filtervfs(vfs, encodedir)
297 self.opener = self.vfs
297 298
298 299 def join(self, f):
299 300 return self.path + '/' + encodedir(f)
300 301
301 302 def _walk(self, relpath, recurse):
302 303 '''yields (unencoded, encoded, size)'''
303 304 path = self.path
304 305 if relpath:
305 306 path += '/' + relpath
306 307 striplen = len(self.path) + 1
307 308 l = []
308 309 if os.path.isdir(path):
309 310 visit = [path]
310 311 while visit:
311 312 p = visit.pop()
312 313 for f, kind, st in osutil.listdir(p, stat=True):
313 314 fp = p + '/' + f
314 315 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
315 316 n = util.pconvert(fp[striplen:])
316 317 l.append((decodedir(n), n, st.st_size))
317 318 elif kind == stat.S_IFDIR and recurse:
318 319 visit.append(fp)
319 320 l.sort()
320 321 return l
321 322
322 323 def datafiles(self):
323 324 return self._walk('data', True)
324 325
325 326 def walk(self):
326 327 '''yields (unencoded, encoded, size)'''
327 328 # yield data files first
328 329 for x in self.datafiles():
329 330 yield x
330 331 # yield manifest before changelog
331 332 for x in reversed(self._walk('', False)):
332 333 yield x
333 334
334 335 def copylist(self):
335 336 return ['requires'] + _data.split()
336 337
337 338 def write(self):
338 339 pass
339 340
340 341 class encodedstore(basicstore):
341 342 def __init__(self, path, vfstype):
342 343 self.path = path + '/store'
343 344 self.createmode = _calcmode(self.path)
344 345 vfs = vfstype(self.path)
345 346 vfs.createmode = self.createmode
346 self.opener = scmutil.filteropener(vfs, encodefilename)
347 self.vfs = scmutil.filtervfs(vfs, encodefilename)
348 self.opener = self.vfs
347 349
348 350 def datafiles(self):
349 351 for a, b, size in self._walk('data', True):
350 352 try:
351 353 a = decodefilename(a)
352 354 except KeyError:
353 355 a = None
354 356 yield a, b, size
355 357
356 358 def join(self, f):
357 359 return self.path + '/' + encodefilename(f)
358 360
359 361 def copylist(self):
360 362 return (['requires', '00changelog.i'] +
361 363 ['store/' + f for f in _data.split()])
362 364
363 365 class fncache(object):
364 366 # the filename used to be partially encoded
365 367 # hence the encodedir/decodedir dance
366 368 def __init__(self, opener):
367 369 self.opener = opener
368 370 self.entries = None
369 371 self._dirty = False
370 372
371 373 def _load(self):
372 374 '''fill the entries from the fncache file'''
373 375 self._dirty = False
374 376 try:
375 377 fp = self.opener('fncache', mode='rb')
376 378 except IOError:
377 379 # skip nonexistent file
378 380 self.entries = set()
379 381 return
380 382 self.entries = set(decodedir(fp.read()).splitlines())
381 383 if '' in self.entries:
382 384 fp.seek(0)
383 385 for n, line in enumerate(fp):
384 386 if not line.rstrip('\n'):
385 387 t = _('invalid entry in fncache, line %s') % (n + 1)
386 388 raise util.Abort(t)
387 389 fp.close()
388 390
389 391 def _write(self, files, atomictemp):
390 392 fp = self.opener('fncache', mode='wb', atomictemp=atomictemp)
391 393 if files:
392 394 fp.write(encodedir('\n'.join(files) + '\n'))
393 395 fp.close()
394 396 self._dirty = False
395 397
396 398 def rewrite(self, files):
397 399 self._write(files, False)
398 400 self.entries = set(files)
399 401
400 402 def write(self):
401 403 if self._dirty:
402 404 self._write(self.entries, True)
403 405
404 406 def add(self, fn):
405 407 if self.entries is None:
406 408 self._load()
407 409 if fn not in self.entries:
408 410 self._dirty = True
409 411 self.entries.add(fn)
410 412
411 413 def __contains__(self, fn):
412 414 if self.entries is None:
413 415 self._load()
414 416 return fn in self.entries
415 417
416 418 def __iter__(self):
417 419 if self.entries is None:
418 420 self._load()
419 421 return iter(self.entries)
420 422
421 423 class _fncachevfs(scmutil.abstractvfs):
422 424 def __init__(self, op, fnc, encode):
423 425 self.opener = op
424 426 self.fncache = fnc
425 427 self.encode = encode
426 428
427 429 def _getmustaudit(self):
428 430 return self.opener.mustaudit
429 431
430 432 def _setmustaudit(self, onoff):
431 433 self.opener.mustaudit = onoff
432 434
433 435 mustaudit = property(_getmustaudit, _setmustaudit)
434 436
435 437 def __call__(self, path, mode='r', *args, **kw):
436 438 if mode not in ('r', 'rb') and path.startswith('data/'):
437 439 self.fncache.add(path)
438 440 return self.opener(self.encode(path), mode, *args, **kw)
439 441
440 442 class fncachestore(basicstore):
441 443 def __init__(self, path, vfstype, dotencode):
442 444 if dotencode:
443 445 encode = _dothybridencode
444 446 else:
445 447 encode = _plainhybridencode
446 448 self.encode = encode
447 449 self.path = path + '/store'
448 450 self.pathsep = self.path + '/'
449 451 self.createmode = _calcmode(self.path)
450 452 vfs = vfstype(self.path)
451 453 vfs.createmode = self.createmode
452 454 fnc = fncache(vfs)
453 455 self.fncache = fnc
454 self.opener = _fncachevfs(vfs, fnc, encode)
456 self.vfs = _fncachevfs(vfs, fnc, encode)
457 self.opener = self.vfs
455 458
456 459 def join(self, f):
457 460 return self.pathsep + self.encode(f)
458 461
459 462 def getsize(self, path):
460 463 return os.stat(self.pathsep + path).st_size
461 464
462 465 def datafiles(self):
463 466 rewrite = False
464 467 existing = []
465 468 for f in sorted(self.fncache):
466 469 ef = self.encode(f)
467 470 try:
468 471 yield f, ef, self.getsize(ef)
469 472 existing.append(f)
470 473 except OSError, err:
471 474 if err.errno != errno.ENOENT:
472 475 raise
473 476 # nonexistent entry
474 477 rewrite = True
475 478 if rewrite:
476 479 # rewrite fncache to remove nonexistent entries
477 480 # (may be caused by rollback / strip)
478 481 self.fncache.rewrite(existing)
479 482
480 483 def copylist(self):
481 484 d = ('data dh fncache phaseroots obsstore'
482 485 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
483 486 return (['requires', '00changelog.i'] +
484 487 ['store/' + f for f in d.split()])
485 488
486 489 def write(self):
487 490 self.fncache.write()
488 491
489 492 def store(requirements, path, vfstype):
490 493 if 'store' in requirements:
491 494 if 'fncache' in requirements:
492 495 return fncachestore(path, vfstype, 'dotencode' in requirements)
493 496 return encodedstore(path, vfstype)
494 497 return basicstore(path, vfstype)
General Comments 0
You need to be logged in to leave comments. Login now