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