##// END OF EJS Templates
add filteropener abstraction for store openers
Dan Villiom Podlaski Christiansen -
r14090:e24b5e3c default
parent child Browse files
Show More
@@ -1,431 +1,441 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 util, error, osutil
10 10 import os, errno, stat, sys
11 11
12 12 def checkfilename(f):
13 13 '''Check that the filename f is an acceptable filename for a tracked file'''
14 14 if '\r' in f or '\n' in f:
15 15 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
16 16
17 17 def checkportable(ui, f):
18 18 '''Check if filename f is portable and warn or abort depending on config'''
19 19 checkfilename(f)
20 20 if showportabilityalert(ui):
21 21 msg = util.checkwinfilename(f)
22 22 if msg:
23 23 portabilityalert(ui, "%s: %r" % (msg, f))
24 24
25 25 def checkcasecollision(ui, f, files):
26 26 if f.lower() in files and files[f.lower()] != f:
27 27 portabilityalert(ui, _('possible case-folding collision for %s') % f)
28 28 files[f.lower()] = f
29 29
30 30 def checkportabilityalert(ui):
31 31 '''check if the user's config requests nothing, a warning, or abort for
32 32 non-portable filenames'''
33 33 val = ui.config('ui', 'portablefilenames', 'warn')
34 34 lval = val.lower()
35 35 bval = util.parsebool(val)
36 36 abort = os.name == 'nt' or lval == 'abort'
37 37 warn = bval or lval == 'warn'
38 38 if bval is None and not (warn or abort or lval == 'ignore'):
39 39 raise error.ConfigError(
40 40 _("ui.portablefilenames value is invalid ('%s')") % val)
41 41 return abort, warn
42 42
43 43 def showportabilityalert(ui):
44 44 '''check if the user wants any notification of portability problems'''
45 45 abort, warn = checkportabilityalert(ui)
46 46 return abort or warn
47 47
48 48 def portabilityalert(ui, msg):
49 49 if not msg:
50 50 return
51 51 abort, warn = checkportabilityalert(ui)
52 52 if abort:
53 53 raise util.Abort("%s" % msg)
54 54 elif warn:
55 55 ui.warn(_("warning: %s\n") % msg)
56 56
57 57 class path_auditor(object):
58 58 '''ensure that a filesystem path contains no banned components.
59 59 the following properties of a path are checked:
60 60
61 61 - ends with a directory separator
62 62 - under top-level .hg
63 63 - starts at the root of a windows drive
64 64 - contains ".."
65 65 - traverses a symlink (e.g. a/symlink_here/b)
66 66 - inside a nested repository (a callback can be used to approve
67 67 some nested repositories, e.g., subrepositories)
68 68 '''
69 69
70 70 def __init__(self, root, callback=None):
71 71 self.audited = set()
72 72 self.auditeddir = set()
73 73 self.root = root
74 74 self.callback = callback
75 75
76 76 def __call__(self, path):
77 77 '''Check the relative path.
78 78 path may contain a pattern (e.g. foodir/**.txt)'''
79 79
80 80 if path in self.audited:
81 81 return
82 82 # AIX ignores "/" at end of path, others raise EISDIR.
83 83 if util.endswithsep(path):
84 84 raise util.Abort(_("path ends in directory separator: %s") % path)
85 85 normpath = os.path.normcase(path)
86 86 parts = util.splitpath(normpath)
87 87 if (os.path.splitdrive(path)[0]
88 88 or parts[0].lower() in ('.hg', '.hg.', '')
89 89 or os.pardir in parts):
90 90 raise util.Abort(_("path contains illegal component: %s") % path)
91 91 if '.hg' in path.lower():
92 92 lparts = [p.lower() for p in parts]
93 93 for p in '.hg', '.hg.':
94 94 if p in lparts[1:]:
95 95 pos = lparts.index(p)
96 96 base = os.path.join(*parts[:pos])
97 97 raise util.Abort(_('path %r is inside nested repo %r')
98 98 % (path, base))
99 99
100 100 parts.pop()
101 101 prefixes = []
102 102 while parts:
103 103 prefix = os.sep.join(parts)
104 104 if prefix in self.auditeddir:
105 105 break
106 106 curpath = os.path.join(self.root, prefix)
107 107 try:
108 108 st = os.lstat(curpath)
109 109 except OSError, err:
110 110 # EINVAL can be raised as invalid path syntax under win32.
111 111 # They must be ignored for patterns can be checked too.
112 112 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
113 113 raise
114 114 else:
115 115 if stat.S_ISLNK(st.st_mode):
116 116 raise util.Abort(
117 117 _('path %r traverses symbolic link %r')
118 118 % (path, prefix))
119 119 elif (stat.S_ISDIR(st.st_mode) and
120 120 os.path.isdir(os.path.join(curpath, '.hg'))):
121 121 if not self.callback or not self.callback(curpath):
122 122 raise util.Abort(_('path %r is inside nested repo %r') %
123 123 (path, prefix))
124 124 prefixes.append(prefix)
125 125 parts.pop()
126 126
127 127 self.audited.add(path)
128 128 # only add prefixes to the cache after checking everything: we don't
129 129 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
130 130 self.auditeddir.update(prefixes)
131 131
132 132 class abstractopener(object):
133 133 """Abstract base class; cannot be instantiated"""
134 134
135 135 def __init__(self, *args, **kwargs):
136 136 '''Prevent instantiation; don't call this from subclasses.'''
137 137 raise NotImplementedError('attempted instantiating ' + str(type(self)))
138 138
139 139 class opener(abstractopener):
140 140 '''Open files relative to a base directory
141 141
142 142 This class is used to hide the details of COW semantics and
143 143 remote file access from higher level code.
144 144 '''
145 145 def __init__(self, base, audit=True):
146 146 self.base = base
147 147 if audit:
148 148 self.auditor = path_auditor(base)
149 149 else:
150 150 self.auditor = util.always
151 151 self.createmode = None
152 152 self._trustnlink = None
153 153
154 154 @util.propertycache
155 155 def _can_symlink(self):
156 156 return util.checklink(self.base)
157 157
158 158 def _fixfilemode(self, name):
159 159 if self.createmode is None:
160 160 return
161 161 os.chmod(name, self.createmode & 0666)
162 162
163 163 def __call__(self, path, mode="r", text=False, atomictemp=False):
164 164 r = util.checkosfilename(path)
165 165 if r:
166 166 raise util.Abort("%s: %r" % (r, path))
167 167 self.auditor(path)
168 168 f = os.path.join(self.base, path)
169 169
170 170 if not text and "b" not in mode:
171 171 mode += "b" # for that other OS
172 172
173 173 nlink = -1
174 174 dirname, basename = os.path.split(f)
175 175 # If basename is empty, then the path is malformed because it points
176 176 # to a directory. Let the posixfile() call below raise IOError.
177 177 if basename and mode not in ('r', 'rb'):
178 178 if atomictemp:
179 179 if not os.path.isdir(dirname):
180 180 util.makedirs(dirname, self.createmode)
181 181 return util.atomictempfile(f, mode, self.createmode)
182 182 try:
183 183 if 'w' in mode:
184 184 util.unlink(f)
185 185 nlink = 0
186 186 else:
187 187 # nlinks() may behave differently for files on Windows
188 188 # shares if the file is open.
189 189 fd = util.posixfile(f)
190 190 nlink = util.nlinks(f)
191 191 if nlink < 1:
192 192 nlink = 2 # force mktempcopy (issue1922)
193 193 fd.close()
194 194 except (OSError, IOError), e:
195 195 if e.errno != errno.ENOENT:
196 196 raise
197 197 nlink = 0
198 198 if not os.path.isdir(dirname):
199 199 util.makedirs(dirname, self.createmode)
200 200 if nlink > 0:
201 201 if self._trustnlink is None:
202 202 self._trustnlink = nlink > 1 or util.checknlink(f)
203 203 if nlink > 1 or not self._trustnlink:
204 204 util.rename(util.mktempcopy(f), f)
205 205 fp = util.posixfile(f, mode)
206 206 if nlink == 0:
207 207 self._fixfilemode(f)
208 208 return fp
209 209
210 210 def symlink(self, src, dst):
211 211 self.auditor(dst)
212 212 linkname = os.path.join(self.base, dst)
213 213 try:
214 214 os.unlink(linkname)
215 215 except OSError:
216 216 pass
217 217
218 218 dirname = os.path.dirname(linkname)
219 219 if not os.path.exists(dirname):
220 220 util.makedirs(dirname, self.createmode)
221 221
222 222 if self._can_symlink:
223 223 try:
224 224 os.symlink(src, linkname)
225 225 except OSError, err:
226 226 raise OSError(err.errno, _('could not symlink to %r: %s') %
227 227 (src, err.strerror), linkname)
228 228 else:
229 229 f = self(dst, "w")
230 230 f.write(src)
231 231 f.close()
232 232 self._fixfilemode(dst)
233 233
234 class filteropener(abstractopener):
235 '''Wrapper opener for filtering filenames with a function.'''
236
237 def __init__(self, opener, filter):
238 self._filter = filter
239 self._orig = opener
240
241 def __call__(self, path, *args, **kwargs):
242 return self._orig(self._filter(path), *args, **kwargs)
243
234 244 def canonpath(root, cwd, myname, auditor=None):
235 245 '''return the canonical path of myname, given cwd and root'''
236 246 if util.endswithsep(root):
237 247 rootsep = root
238 248 else:
239 249 rootsep = root + os.sep
240 250 name = myname
241 251 if not os.path.isabs(name):
242 252 name = os.path.join(root, cwd, name)
243 253 name = os.path.normpath(name)
244 254 if auditor is None:
245 255 auditor = path_auditor(root)
246 256 if name != rootsep and name.startswith(rootsep):
247 257 name = name[len(rootsep):]
248 258 auditor(name)
249 259 return util.pconvert(name)
250 260 elif name == root:
251 261 return ''
252 262 else:
253 263 # Determine whether `name' is in the hierarchy at or beneath `root',
254 264 # by iterating name=dirname(name) until that causes no change (can't
255 265 # check name == '/', because that doesn't work on windows). For each
256 266 # `name', compare dev/inode numbers. If they match, the list `rel'
257 267 # holds the reversed list of components making up the relative file
258 268 # name we want.
259 269 root_st = os.stat(root)
260 270 rel = []
261 271 while True:
262 272 try:
263 273 name_st = os.stat(name)
264 274 except OSError:
265 275 break
266 276 if util.samestat(name_st, root_st):
267 277 if not rel:
268 278 # name was actually the same as root (maybe a symlink)
269 279 return ''
270 280 rel.reverse()
271 281 name = os.path.join(*rel)
272 282 auditor(name)
273 283 return util.pconvert(name)
274 284 dirname, basename = os.path.split(name)
275 285 rel.append(basename)
276 286 if dirname == name:
277 287 break
278 288 name = dirname
279 289
280 290 raise util.Abort('%s not under root' % myname)
281 291
282 292 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
283 293 '''yield every hg repository under path, recursively.'''
284 294 def errhandler(err):
285 295 if err.filename == path:
286 296 raise err
287 297 if followsym and hasattr(os.path, 'samestat'):
288 298 def _add_dir_if_not_there(dirlst, dirname):
289 299 match = False
290 300 samestat = os.path.samestat
291 301 dirstat = os.stat(dirname)
292 302 for lstdirstat in dirlst:
293 303 if samestat(dirstat, lstdirstat):
294 304 match = True
295 305 break
296 306 if not match:
297 307 dirlst.append(dirstat)
298 308 return not match
299 309 else:
300 310 followsym = False
301 311
302 312 if (seen_dirs is None) and followsym:
303 313 seen_dirs = []
304 314 _add_dir_if_not_there(seen_dirs, path)
305 315 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
306 316 dirs.sort()
307 317 if '.hg' in dirs:
308 318 yield root # found a repository
309 319 qroot = os.path.join(root, '.hg', 'patches')
310 320 if os.path.isdir(os.path.join(qroot, '.hg')):
311 321 yield qroot # we have a patch queue repo here
312 322 if recurse:
313 323 # avoid recursing inside the .hg directory
314 324 dirs.remove('.hg')
315 325 else:
316 326 dirs[:] = [] # don't descend further
317 327 elif followsym:
318 328 newdirs = []
319 329 for d in dirs:
320 330 fname = os.path.join(root, d)
321 331 if _add_dir_if_not_there(seen_dirs, fname):
322 332 if os.path.islink(fname):
323 333 for hgname in walkrepos(fname, True, seen_dirs):
324 334 yield hgname
325 335 else:
326 336 newdirs.append(d)
327 337 dirs[:] = newdirs
328 338
329 339 def os_rcpath():
330 340 '''return default os-specific hgrc search path'''
331 341 path = system_rcpath()
332 342 path.extend(user_rcpath())
333 343 path = [os.path.normpath(f) for f in path]
334 344 return path
335 345
336 346 _rcpath = None
337 347
338 348 def rcpath():
339 349 '''return hgrc search path. if env var HGRCPATH is set, use it.
340 350 for each item in path, if directory, use files ending in .rc,
341 351 else use item.
342 352 make HGRCPATH empty to only look in .hg/hgrc of current repo.
343 353 if no HGRCPATH, use default os-specific path.'''
344 354 global _rcpath
345 355 if _rcpath is None:
346 356 if 'HGRCPATH' in os.environ:
347 357 _rcpath = []
348 358 for p in os.environ['HGRCPATH'].split(os.pathsep):
349 359 if not p:
350 360 continue
351 361 p = util.expandpath(p)
352 362 if os.path.isdir(p):
353 363 for f, kind in osutil.listdir(p):
354 364 if f.endswith('.rc'):
355 365 _rcpath.append(os.path.join(p, f))
356 366 else:
357 367 _rcpath.append(p)
358 368 else:
359 369 _rcpath = os_rcpath()
360 370 return _rcpath
361 371
362 372 if os.name != 'nt':
363 373
364 374 def rcfiles(path):
365 375 rcs = [os.path.join(path, 'hgrc')]
366 376 rcdir = os.path.join(path, 'hgrc.d')
367 377 try:
368 378 rcs.extend([os.path.join(rcdir, f)
369 379 for f, kind in osutil.listdir(rcdir)
370 380 if f.endswith(".rc")])
371 381 except OSError:
372 382 pass
373 383 return rcs
374 384
375 385 def system_rcpath():
376 386 path = []
377 387 # old mod_python does not set sys.argv
378 388 if len(getattr(sys, 'argv', [])) > 0:
379 389 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
380 390 '/../etc/mercurial'))
381 391 path.extend(rcfiles('/etc/mercurial'))
382 392 return path
383 393
384 394 def user_rcpath():
385 395 return [os.path.expanduser('~/.hgrc')]
386 396
387 397 else:
388 398
389 399 _HKEY_LOCAL_MACHINE = 0x80000002L
390 400
391 401 def system_rcpath():
392 402 '''return default os-specific hgrc search path'''
393 403 rcpath = []
394 404 filename = util.executable_path()
395 405 # Use mercurial.ini found in directory with hg.exe
396 406 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
397 407 if os.path.isfile(progrc):
398 408 rcpath.append(progrc)
399 409 return rcpath
400 410 # Use hgrc.d found in directory with hg.exe
401 411 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
402 412 if os.path.isdir(progrcd):
403 413 for f, kind in osutil.listdir(progrcd):
404 414 if f.endswith('.rc'):
405 415 rcpath.append(os.path.join(progrcd, f))
406 416 return rcpath
407 417 # else look for a system rcpath in the registry
408 418 value = util.lookup_reg('SOFTWARE\\Mercurial', None,
409 419 _HKEY_LOCAL_MACHINE)
410 420 if not isinstance(value, str) or not value:
411 421 return rcpath
412 422 value = value.replace('/', os.sep)
413 423 for p in value.split(os.pathsep):
414 424 if p.lower().endswith('mercurial.ini'):
415 425 rcpath.append(p)
416 426 elif os.path.isdir(p):
417 427 for f, kind in osutil.listdir(p):
418 428 if f.endswith('.rc'):
419 429 rcpath.append(os.path.join(p, f))
420 430 return rcpath
421 431
422 432 def user_rcpath():
423 433 '''return os-specific hgrc search path to the user dir'''
424 434 home = os.path.expanduser('~')
425 435 path = [os.path.join(home, 'mercurial.ini'),
426 436 os.path.join(home, '.hgrc')]
427 437 userprofile = os.environ.get('USERPROFILE')
428 438 if userprofile:
429 439 path.append(os.path.join(userprofile, 'mercurial.ini'))
430 440 path.append(os.path.join(userprofile, '.hgrc'))
431 441 return path
@@ -1,421 +1,421 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 import osutil, util
9 import osutil, scmutil, util
10 10 import os, stat
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 25 if not path.startswith('data/'):
26 26 return path
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 def decodedir(path):
33 33 '''
34 34 >>> decodedir('data/foo.i')
35 35 'data/foo.i'
36 36 >>> decodedir('data/foo.i.hg/bla.i')
37 37 'data/foo.i/bla.i'
38 38 >>> decodedir('data/foo.i.hg.hg/bla.i')
39 39 'data/foo.i.hg/bla.i'
40 40 '''
41 41 if not path.startswith('data/') or ".hg/" not in path:
42 42 return path
43 43 return (path
44 44 .replace(".d.hg/", ".d/")
45 45 .replace(".i.hg/", ".i/")
46 46 .replace(".hg.hg/", ".hg/"))
47 47
48 48 def _buildencodefun():
49 49 '''
50 50 >>> enc, dec = _buildencodefun()
51 51
52 52 >>> enc('nothing/special.txt')
53 53 'nothing/special.txt'
54 54 >>> dec('nothing/special.txt')
55 55 'nothing/special.txt'
56 56
57 57 >>> enc('HELLO')
58 58 '_h_e_l_l_o'
59 59 >>> dec('_h_e_l_l_o')
60 60 'HELLO'
61 61
62 62 >>> enc('hello:world?')
63 63 'hello~3aworld~3f'
64 64 >>> dec('hello~3aworld~3f')
65 65 'hello:world?'
66 66
67 67 >>> enc('the\x07quick\xADshot')
68 68 'the~07quick~adshot'
69 69 >>> dec('the~07quick~adshot')
70 70 'the\\x07quick\\xadshot'
71 71 '''
72 72 e = '_'
73 73 win_reserved = [ord(x) for x in '\\:*?"<>|']
74 74 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
75 75 for x in (range(32) + range(126, 256) + win_reserved):
76 76 cmap[chr(x)] = "~%02x" % x
77 77 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
78 78 cmap[chr(x)] = e + chr(x).lower()
79 79 dmap = {}
80 80 for k, v in cmap.iteritems():
81 81 dmap[v] = k
82 82 def decode(s):
83 83 i = 0
84 84 while i < len(s):
85 85 for l in xrange(1, 4):
86 86 try:
87 87 yield dmap[s[i:i + l]]
88 88 i += l
89 89 break
90 90 except KeyError:
91 91 pass
92 92 else:
93 93 raise KeyError
94 94 return (lambda s: "".join([cmap[c] for c in encodedir(s)]),
95 95 lambda s: decodedir("".join(list(decode(s)))))
96 96
97 97 encodefilename, decodefilename = _buildencodefun()
98 98
99 99 def _build_lower_encodefun():
100 100 '''
101 101 >>> f = _build_lower_encodefun()
102 102 >>> f('nothing/special.txt')
103 103 'nothing/special.txt'
104 104 >>> f('HELLO')
105 105 'hello'
106 106 >>> f('hello:world?')
107 107 'hello~3aworld~3f'
108 108 >>> f('the\x07quick\xADshot')
109 109 'the~07quick~adshot'
110 110 '''
111 111 win_reserved = [ord(x) for x in '\\:*?"<>|']
112 112 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
113 113 for x in (range(32) + range(126, 256) + win_reserved):
114 114 cmap[chr(x)] = "~%02x" % x
115 115 for x in range(ord("A"), ord("Z")+1):
116 116 cmap[chr(x)] = chr(x).lower()
117 117 return lambda s: "".join([cmap[c] for c in s])
118 118
119 119 lowerencode = _build_lower_encodefun()
120 120
121 121 _windows_reserved_filenames = '''con prn aux nul
122 122 com1 com2 com3 com4 com5 com6 com7 com8 com9
123 123 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
124 124 def _auxencode(path, dotencode):
125 125 '''
126 126 Encodes filenames containing names reserved by Windows or which end in
127 127 period or space. Does not touch other single reserved characters c.
128 128 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
129 129 Additionally encodes space or period at the beginning, if dotencode is
130 130 True.
131 131 path is assumed to be all lowercase.
132 132
133 133 >>> _auxencode('.foo/aux.txt/txt.aux/con/prn/nul/foo.', True)
134 134 '~2efoo/au~78.txt/txt.aux/co~6e/pr~6e/nu~6c/foo~2e'
135 135 >>> _auxencode('.com1com2/lpt9.lpt4.lpt1/conprn/foo.', False)
136 136 '.com1com2/lp~749.lpt4.lpt1/conprn/foo~2e'
137 137 >>> _auxencode('foo. ', True)
138 138 'foo.~20'
139 139 >>> _auxencode(' .foo', True)
140 140 '~20.foo'
141 141 '''
142 142 res = []
143 143 for n in path.split('/'):
144 144 if n:
145 145 base = n.split('.')[0]
146 146 if base and (base in _windows_reserved_filenames):
147 147 # encode third letter ('aux' -> 'au~78')
148 148 ec = "~%02x" % ord(n[2])
149 149 n = n[0:2] + ec + n[3:]
150 150 if n[-1] in '. ':
151 151 # encode last period or space ('foo...' -> 'foo..~2e')
152 152 n = n[:-1] + "~%02x" % ord(n[-1])
153 153 if dotencode and n[0] in '. ':
154 154 n = "~%02x" % ord(n[0]) + n[1:]
155 155 res.append(n)
156 156 return '/'.join(res)
157 157
158 158 MAX_PATH_LEN_IN_HGSTORE = 120
159 159 DIR_PREFIX_LEN = 8
160 160 _MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
161 161 def _hybridencode(path, auxencode):
162 162 '''encodes path with a length limit
163 163
164 164 Encodes all paths that begin with 'data/', according to the following.
165 165
166 166 Default encoding (reversible):
167 167
168 168 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
169 169 characters are encoded as '~xx', where xx is the two digit hex code
170 170 of the character (see encodefilename).
171 171 Relevant path components consisting of Windows reserved filenames are
172 172 masked by encoding the third character ('aux' -> 'au~78', see auxencode).
173 173
174 174 Hashed encoding (not reversible):
175 175
176 176 If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a
177 177 non-reversible hybrid hashing of the path is done instead.
178 178 This encoding uses up to DIR_PREFIX_LEN characters of all directory
179 179 levels of the lowerencoded path, but not more levels than can fit into
180 180 _MAX_SHORTENED_DIRS_LEN.
181 181 Then follows the filler followed by the sha digest of the full path.
182 182 The filler is the beginning of the basename of the lowerencoded path
183 183 (the basename is everything after the last path separator). The filler
184 184 is as long as possible, filling in characters from the basename until
185 185 the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars
186 186 of the basename have been taken).
187 187 The extension (e.g. '.i' or '.d') is preserved.
188 188
189 189 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
190 190 encoding was used.
191 191 '''
192 192 if not path.startswith('data/'):
193 193 return path
194 194 # escape directories ending with .i and .d
195 195 path = encodedir(path)
196 196 ndpath = path[len('data/'):]
197 197 res = 'data/' + auxencode(encodefilename(ndpath))
198 198 if len(res) > MAX_PATH_LEN_IN_HGSTORE:
199 199 digest = _sha(path).hexdigest()
200 200 aep = auxencode(lowerencode(ndpath))
201 201 _root, ext = os.path.splitext(aep)
202 202 parts = aep.split('/')
203 203 basename = parts[-1]
204 204 sdirs = []
205 205 for p in parts[:-1]:
206 206 d = p[:DIR_PREFIX_LEN]
207 207 if d[-1] in '. ':
208 208 # Windows can't access dirs ending in period or space
209 209 d = d[:-1] + '_'
210 210 t = '/'.join(sdirs) + '/' + d
211 211 if len(t) > _MAX_SHORTENED_DIRS_LEN:
212 212 break
213 213 sdirs.append(d)
214 214 dirs = '/'.join(sdirs)
215 215 if len(dirs) > 0:
216 216 dirs += '/'
217 217 res = 'dh/' + dirs + digest + ext
218 218 space_left = MAX_PATH_LEN_IN_HGSTORE - len(res)
219 219 if space_left > 0:
220 220 filler = basename[:space_left]
221 221 res = 'dh/' + dirs + filler + digest + ext
222 222 return res
223 223
224 224 def _calcmode(path):
225 225 try:
226 226 # files in .hg/ will be created using this mode
227 227 mode = os.stat(path).st_mode
228 228 # avoid some useless chmods
229 229 if (0777 & ~util.umask) == (0777 & mode):
230 230 mode = None
231 231 except OSError:
232 232 mode = None
233 233 return mode
234 234
235 235 _data = 'data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
236 236
237 237 class basicstore(object):
238 238 '''base class for local repository stores'''
239 239 def __init__(self, path, opener):
240 240 self.path = path
241 241 self.createmode = _calcmode(path)
242 242 op = opener(self.path)
243 243 op.createmode = self.createmode
244 self.opener = lambda f, *args, **kw: op(encodedir(f), *args, **kw)
244 self.opener = scmutil.filteropener(op, encodedir)
245 245
246 246 def join(self, f):
247 247 return self.path + '/' + encodedir(f)
248 248
249 249 def _walk(self, relpath, recurse):
250 250 '''yields (unencoded, encoded, size)'''
251 251 path = self.path
252 252 if relpath:
253 253 path += '/' + relpath
254 254 striplen = len(self.path) + 1
255 255 l = []
256 256 if os.path.isdir(path):
257 257 visit = [path]
258 258 while visit:
259 259 p = visit.pop()
260 260 for f, kind, st in osutil.listdir(p, stat=True):
261 261 fp = p + '/' + f
262 262 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
263 263 n = util.pconvert(fp[striplen:])
264 264 l.append((decodedir(n), n, st.st_size))
265 265 elif kind == stat.S_IFDIR and recurse:
266 266 visit.append(fp)
267 267 return sorted(l)
268 268
269 269 def datafiles(self):
270 270 return self._walk('data', True)
271 271
272 272 def walk(self):
273 273 '''yields (unencoded, encoded, size)'''
274 274 # yield data files first
275 275 for x in self.datafiles():
276 276 yield x
277 277 # yield manifest before changelog
278 278 for x in reversed(self._walk('', False)):
279 279 yield x
280 280
281 281 def copylist(self):
282 282 return ['requires'] + _data.split()
283 283
284 284 def write(self):
285 285 pass
286 286
287 287 class encodedstore(basicstore):
288 288 def __init__(self, path, opener):
289 289 self.path = path + '/store'
290 290 self.createmode = _calcmode(self.path)
291 291 op = opener(self.path)
292 292 op.createmode = self.createmode
293 self.opener = lambda f, *args, **kw: op(encodefilename(f), *args, **kw)
293 self.opener = scmutil.filteropener(op, encodefilename)
294 294
295 295 def datafiles(self):
296 296 for a, b, size in self._walk('data', True):
297 297 try:
298 298 a = decodefilename(a)
299 299 except KeyError:
300 300 a = None
301 301 yield a, b, size
302 302
303 303 def join(self, f):
304 304 return self.path + '/' + encodefilename(f)
305 305
306 306 def copylist(self):
307 307 return (['requires', '00changelog.i'] +
308 308 ['store/' + f for f in _data.split()])
309 309
310 310 class fncache(object):
311 311 # the filename used to be partially encoded
312 312 # hence the encodedir/decodedir dance
313 313 def __init__(self, opener):
314 314 self.opener = opener
315 315 self.entries = None
316 316 self._dirty = False
317 317
318 318 def _load(self):
319 319 '''fill the entries from the fncache file'''
320 320 self.entries = set()
321 321 self._dirty = False
322 322 try:
323 323 fp = self.opener('fncache', mode='rb')
324 324 except IOError:
325 325 # skip nonexistent file
326 326 return
327 327 for n, line in enumerate(fp):
328 328 if (len(line) < 2) or (line[-1] != '\n'):
329 329 t = _('invalid entry in fncache, line %s') % (n + 1)
330 330 raise util.Abort(t)
331 331 self.entries.add(decodedir(line[:-1]))
332 332 fp.close()
333 333
334 334 def rewrite(self, files):
335 335 fp = self.opener('fncache', mode='wb')
336 336 for p in files:
337 337 fp.write(encodedir(p) + '\n')
338 338 fp.close()
339 339 self.entries = set(files)
340 340 self._dirty = False
341 341
342 342 def write(self):
343 343 if not self._dirty:
344 344 return
345 345 fp = self.opener('fncache', mode='wb', atomictemp=True)
346 346 for p in self.entries:
347 347 fp.write(encodedir(p) + '\n')
348 348 fp.rename()
349 349 self._dirty = False
350 350
351 351 def add(self, fn):
352 352 if self.entries is None:
353 353 self._load()
354 354 if fn not in self.entries:
355 355 self._dirty = True
356 356 self.entries.add(fn)
357 357
358 358 def __contains__(self, fn):
359 359 if self.entries is None:
360 360 self._load()
361 361 return fn in self.entries
362 362
363 363 def __iter__(self):
364 364 if self.entries is None:
365 365 self._load()
366 366 return iter(self.entries)
367 367
368 368 class fncachestore(basicstore):
369 369 def __init__(self, path, opener, encode):
370 370 self.encode = encode
371 371 self.path = path + '/store'
372 372 self.createmode = _calcmode(self.path)
373 373 op = opener(self.path)
374 374 op.createmode = self.createmode
375 375 fnc = fncache(op)
376 376 self.fncache = fnc
377 377
378 378 def fncacheopener(path, mode='r', *args, **kw):
379 379 if mode not in ('r', 'rb') and path.startswith('data/'):
380 380 fnc.add(path)
381 381 return op(self.encode(path), mode, *args, **kw)
382 382 self.opener = fncacheopener
383 383
384 384 def join(self, f):
385 385 return self.path + '/' + self.encode(f)
386 386
387 387 def datafiles(self):
388 388 rewrite = False
389 389 existing = []
390 390 spath = self.path
391 391 for f in self.fncache:
392 392 ef = self.encode(f)
393 393 try:
394 394 st = os.stat(spath + '/' + ef)
395 395 yield f, ef, st.st_size
396 396 existing.append(f)
397 397 except OSError:
398 398 # nonexistent entry
399 399 rewrite = True
400 400 if rewrite:
401 401 # rewrite fncache to remove nonexistent entries
402 402 # (may be caused by rollback / strip)
403 403 self.fncache.rewrite(existing)
404 404
405 405 def copylist(self):
406 406 d = ('data dh fncache'
407 407 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
408 408 return (['requires', '00changelog.i'] +
409 409 ['store/' + f for f in d.split()])
410 410
411 411 def write(self):
412 412 self.fncache.write()
413 413
414 414 def store(requirements, path, opener):
415 415 if 'store' in requirements:
416 416 if 'fncache' in requirements:
417 417 auxencode = lambda f: _auxencode(f, 'dotencode' in requirements)
418 418 encode = lambda f: _hybridencode(f, auxencode)
419 419 return fncachestore(path, opener, encode)
420 420 return encodedstore(path, opener)
421 421 return basicstore(path, opener)
General Comments 0
You need to be logged in to leave comments. Login now