##// END OF EJS Templates
scmutil: abstract out mustaudit delegation
Bryan O'Sullivan -
r17845:408ded42 stable
parent child Browse files
Show More
@@ -1,950 +1,962
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, revset, similar, encoding, phases
10 10 import match as matchmod
11 11 import os, errno, re, stat, sys, glob
12 12
13 13 def nochangesfound(ui, repo, excluded=None):
14 14 '''Report no changes for push/pull, excluded is None or a list of
15 15 nodes excluded from the push/pull.
16 16 '''
17 17 secretlist = []
18 18 if excluded:
19 19 for n in excluded:
20 20 ctx = repo[n]
21 21 if ctx.phase() >= phases.secret and not ctx.extinct():
22 22 secretlist.append(n)
23 23
24 24 if secretlist:
25 25 ui.status(_("no changes found (ignored %d secret changesets)\n")
26 26 % len(secretlist))
27 27 else:
28 28 ui.status(_("no changes found\n"))
29 29
30 30 def checknewlabel(repo, lbl, kind):
31 31 if lbl in ['tip', '.', 'null']:
32 32 raise util.Abort(_("the name '%s' is reserved") % lbl)
33 33 for c in (':', '\0', '\n', '\r'):
34 34 if c in lbl:
35 35 raise util.Abort(_("%r cannot be used in a %s name") %
36 36 (c, kind))
37 37
38 38 def checkfilename(f):
39 39 '''Check that the filename f is an acceptable filename for a tracked file'''
40 40 if '\r' in f or '\n' in f:
41 41 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
42 42
43 43 def checkportable(ui, f):
44 44 '''Check if filename f is portable and warn or abort depending on config'''
45 45 checkfilename(f)
46 46 abort, warn = checkportabilityalert(ui)
47 47 if abort or warn:
48 48 msg = util.checkwinfilename(f)
49 49 if msg:
50 50 msg = "%s: %r" % (msg, f)
51 51 if abort:
52 52 raise util.Abort(msg)
53 53 ui.warn(_("warning: %s\n") % msg)
54 54
55 55 def checkportabilityalert(ui):
56 56 '''check if the user's config requests nothing, a warning, or abort for
57 57 non-portable filenames'''
58 58 val = ui.config('ui', 'portablefilenames', 'warn')
59 59 lval = val.lower()
60 60 bval = util.parsebool(val)
61 61 abort = os.name == 'nt' or lval == 'abort'
62 62 warn = bval or lval == 'warn'
63 63 if bval is None and not (warn or abort or lval == 'ignore'):
64 64 raise error.ConfigError(
65 65 _("ui.portablefilenames value is invalid ('%s')") % val)
66 66 return abort, warn
67 67
68 68 class casecollisionauditor(object):
69 69 def __init__(self, ui, abort, dirstate):
70 70 self._ui = ui
71 71 self._abort = abort
72 72 allfiles = '\0'.join(dirstate._map)
73 73 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
74 74 self._dirstate = dirstate
75 75 # The purpose of _newfiles is so that we don't complain about
76 76 # case collisions if someone were to call this object with the
77 77 # same filename twice.
78 78 self._newfiles = set()
79 79
80 80 def __call__(self, f):
81 81 fl = encoding.lower(f)
82 82 if (fl in self._loweredfiles and f not in self._dirstate and
83 83 f not in self._newfiles):
84 84 msg = _('possible case-folding collision for %s') % f
85 85 if self._abort:
86 86 raise util.Abort(msg)
87 87 self._ui.warn(_("warning: %s\n") % msg)
88 88 self._loweredfiles.add(fl)
89 89 self._newfiles.add(f)
90 90
91 91 class pathauditor(object):
92 92 '''ensure that a filesystem path contains no banned components.
93 93 the following properties of a path are checked:
94 94
95 95 - ends with a directory separator
96 96 - under top-level .hg
97 97 - starts at the root of a windows drive
98 98 - contains ".."
99 99 - traverses a symlink (e.g. a/symlink_here/b)
100 100 - inside a nested repository (a callback can be used to approve
101 101 some nested repositories, e.g., subrepositories)
102 102 '''
103 103
104 104 def __init__(self, root, callback=None):
105 105 self.audited = set()
106 106 self.auditeddir = set()
107 107 self.root = root
108 108 self.callback = callback
109 109 if os.path.lexists(root) and not util.checkcase(root):
110 110 self.normcase = util.normcase
111 111 else:
112 112 self.normcase = lambda x: x
113 113
114 114 def __call__(self, path):
115 115 '''Check the relative path.
116 116 path may contain a pattern (e.g. foodir/**.txt)'''
117 117
118 118 path = util.localpath(path)
119 119 normpath = self.normcase(path)
120 120 if normpath in self.audited:
121 121 return
122 122 # AIX ignores "/" at end of path, others raise EISDIR.
123 123 if util.endswithsep(path):
124 124 raise util.Abort(_("path ends in directory separator: %s") % path)
125 125 parts = util.splitpath(path)
126 126 if (os.path.splitdrive(path)[0]
127 127 or parts[0].lower() in ('.hg', '.hg.', '')
128 128 or os.pardir in parts):
129 129 raise util.Abort(_("path contains illegal component: %s") % path)
130 130 if '.hg' in path.lower():
131 131 lparts = [p.lower() for p in parts]
132 132 for p in '.hg', '.hg.':
133 133 if p in lparts[1:]:
134 134 pos = lparts.index(p)
135 135 base = os.path.join(*parts[:pos])
136 136 raise util.Abort(_("path '%s' is inside nested repo %r")
137 137 % (path, base))
138 138
139 139 normparts = util.splitpath(normpath)
140 140 assert len(parts) == len(normparts)
141 141
142 142 parts.pop()
143 143 normparts.pop()
144 144 prefixes = []
145 145 while parts:
146 146 prefix = os.sep.join(parts)
147 147 normprefix = os.sep.join(normparts)
148 148 if normprefix in self.auditeddir:
149 149 break
150 150 curpath = os.path.join(self.root, prefix)
151 151 try:
152 152 st = os.lstat(curpath)
153 153 except OSError, err:
154 154 # EINVAL can be raised as invalid path syntax under win32.
155 155 # They must be ignored for patterns can be checked too.
156 156 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
157 157 raise
158 158 else:
159 159 if stat.S_ISLNK(st.st_mode):
160 160 raise util.Abort(
161 161 _('path %r traverses symbolic link %r')
162 162 % (path, prefix))
163 163 elif (stat.S_ISDIR(st.st_mode) and
164 164 os.path.isdir(os.path.join(curpath, '.hg'))):
165 165 if not self.callback or not self.callback(curpath):
166 166 raise util.Abort(_("path '%s' is inside nested "
167 167 "repo %r")
168 168 % (path, prefix))
169 169 prefixes.append(normprefix)
170 170 parts.pop()
171 171 normparts.pop()
172 172
173 173 self.audited.add(normpath)
174 174 # only add prefixes to the cache after checking everything: we don't
175 175 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
176 176 self.auditeddir.update(prefixes)
177 177
178 178 class abstractvfs(object):
179 179 """Abstract base class; cannot be instantiated"""
180 180
181 181 def __init__(self, *args, **kwargs):
182 182 '''Prevent instantiation; don't call this from subclasses.'''
183 183 raise NotImplementedError('attempted instantiating ' + str(type(self)))
184 184
185 185 def tryread(self, path):
186 186 '''gracefully return an empty string for missing files'''
187 187 try:
188 188 return self.read(path)
189 189 except IOError, inst:
190 190 if inst.errno != errno.ENOENT:
191 191 raise
192 192 return ""
193 193
194 194 def read(self, path):
195 195 fp = self(path, 'rb')
196 196 try:
197 197 return fp.read()
198 198 finally:
199 199 fp.close()
200 200
201 201 def write(self, path, data):
202 202 fp = self(path, 'wb')
203 203 try:
204 204 return fp.write(data)
205 205 finally:
206 206 fp.close()
207 207
208 208 def append(self, path, data):
209 209 fp = self(path, 'ab')
210 210 try:
211 211 return fp.write(data)
212 212 finally:
213 213 fp.close()
214 214
215 215 def exists(self, path=None):
216 216 return os.path.exists(self.join(path))
217 217
218 218 def isdir(self, path=None):
219 219 return os.path.isdir(self.join(path))
220 220
221 221 def makedir(self, path=None, notindexed=True):
222 222 return util.makedir(self.join(path), notindexed)
223 223
224 224 def makedirs(self, path=None, mode=None):
225 225 return util.makedirs(self.join(path), mode)
226 226
227 227 def mkdir(self, path=None):
228 228 return os.mkdir(self.join(path))
229 229
230 230 def readdir(self, path=None, stat=None, skip=None):
231 231 return osutil.listdir(self.join(path), stat, skip)
232 232
233 233 def stat(self, path=None):
234 234 return os.stat(self.join(path))
235 235
236 236 class vfs(abstractvfs):
237 237 '''Operate files relative to a base directory
238 238
239 239 This class is used to hide the details of COW semantics and
240 240 remote file access from higher level code.
241 241 '''
242 242 def __init__(self, base, audit=True, expand=False):
243 243 if expand:
244 244 base = os.path.realpath(util.expandpath(base))
245 245 self.base = base
246 246 self._setmustaudit(audit)
247 247 self.createmode = None
248 248 self._trustnlink = None
249 249
250 250 def _getmustaudit(self):
251 251 return self._audit
252 252
253 253 def _setmustaudit(self, onoff):
254 254 self._audit = onoff
255 255 if onoff:
256 256 self.auditor = pathauditor(self.base)
257 257 else:
258 258 self.auditor = util.always
259 259
260 260 mustaudit = property(_getmustaudit, _setmustaudit)
261 261
262 262 @util.propertycache
263 263 def _cansymlink(self):
264 264 return util.checklink(self.base)
265 265
266 266 def _fixfilemode(self, name):
267 267 if self.createmode is None:
268 268 return
269 269 os.chmod(name, self.createmode & 0666)
270 270
271 271 def __call__(self, path, mode="r", text=False, atomictemp=False):
272 272 if self._audit:
273 273 r = util.checkosfilename(path)
274 274 if r:
275 275 raise util.Abort("%s: %r" % (r, path))
276 276 self.auditor(path)
277 277 f = self.join(path)
278 278
279 279 if not text and "b" not in mode:
280 280 mode += "b" # for that other OS
281 281
282 282 nlink = -1
283 283 dirname, basename = util.split(f)
284 284 # If basename is empty, then the path is malformed because it points
285 285 # to a directory. Let the posixfile() call below raise IOError.
286 286 if basename and mode not in ('r', 'rb'):
287 287 if atomictemp:
288 288 if not os.path.isdir(dirname):
289 289 util.makedirs(dirname, self.createmode)
290 290 return util.atomictempfile(f, mode, self.createmode)
291 291 try:
292 292 if 'w' in mode:
293 293 util.unlink(f)
294 294 nlink = 0
295 295 else:
296 296 # nlinks() may behave differently for files on Windows
297 297 # shares if the file is open.
298 298 fd = util.posixfile(f)
299 299 nlink = util.nlinks(f)
300 300 if nlink < 1:
301 301 nlink = 2 # force mktempcopy (issue1922)
302 302 fd.close()
303 303 except (OSError, IOError), e:
304 304 if e.errno != errno.ENOENT:
305 305 raise
306 306 nlink = 0
307 307 if not os.path.isdir(dirname):
308 308 util.makedirs(dirname, self.createmode)
309 309 if nlink > 0:
310 310 if self._trustnlink is None:
311 311 self._trustnlink = nlink > 1 or util.checknlink(f)
312 312 if nlink > 1 or not self._trustnlink:
313 313 util.rename(util.mktempcopy(f), f)
314 314 fp = util.posixfile(f, mode)
315 315 if nlink == 0:
316 316 self._fixfilemode(f)
317 317 return fp
318 318
319 319 def symlink(self, src, dst):
320 320 self.auditor(dst)
321 321 linkname = self.join(dst)
322 322 try:
323 323 os.unlink(linkname)
324 324 except OSError:
325 325 pass
326 326
327 327 dirname = os.path.dirname(linkname)
328 328 if not os.path.exists(dirname):
329 329 util.makedirs(dirname, self.createmode)
330 330
331 331 if self._cansymlink:
332 332 try:
333 333 os.symlink(src, linkname)
334 334 except OSError, err:
335 335 raise OSError(err.errno, _('could not symlink to %r: %s') %
336 336 (src, err.strerror), linkname)
337 337 else:
338 338 self.write(dst, src)
339 339
340 340 def audit(self, path):
341 341 self.auditor(path)
342 342
343 343 def join(self, path):
344 344 if path:
345 345 return os.path.join(self.base, path)
346 346 else:
347 347 return self.base
348 348
349 349 opener = vfs
350 350
351 class auditvfs(object):
352 def __init__(self, vfs):
353 self.vfs = vfs
354
355 def _getmustaudit(self):
356 return self.vfs.mustaudit
357
358 def _setmustaudit(self, onoff):
359 self.vfs.mustaudit = onoff
360
361 mustaudit = property(_getmustaudit, _setmustaudit)
362
351 363 class filtervfs(abstractvfs):
352 364 '''Wrapper vfs for filtering filenames with a function.'''
353 365
354 366 def __init__(self, opener, filter):
355 367 self._filter = filter
356 368 self._orig = opener
357 369
358 370 def __call__(self, path, *args, **kwargs):
359 371 return self._orig(self._filter(path), *args, **kwargs)
360 372
361 373 def join(self, path):
362 374 if path:
363 375 return self._orig.join(self._filter(path))
364 376 else:
365 377 return self._orig.join(path)
366 378
367 379 filteropener = filtervfs
368 380
369 381 def canonpath(root, cwd, myname, auditor=None):
370 382 '''return the canonical path of myname, given cwd and root'''
371 383 if util.endswithsep(root):
372 384 rootsep = root
373 385 else:
374 386 rootsep = root + os.sep
375 387 name = myname
376 388 if not os.path.isabs(name):
377 389 name = os.path.join(root, cwd, name)
378 390 name = os.path.normpath(name)
379 391 if auditor is None:
380 392 auditor = pathauditor(root)
381 393 if name != rootsep and name.startswith(rootsep):
382 394 name = name[len(rootsep):]
383 395 auditor(name)
384 396 return util.pconvert(name)
385 397 elif name == root:
386 398 return ''
387 399 else:
388 400 # Determine whether `name' is in the hierarchy at or beneath `root',
389 401 # by iterating name=dirname(name) until that causes no change (can't
390 402 # check name == '/', because that doesn't work on windows). The list
391 403 # `rel' holds the reversed list of components making up the relative
392 404 # file name we want.
393 405 rel = []
394 406 while True:
395 407 try:
396 408 s = util.samefile(name, root)
397 409 except OSError:
398 410 s = False
399 411 if s:
400 412 if not rel:
401 413 # name was actually the same as root (maybe a symlink)
402 414 return ''
403 415 rel.reverse()
404 416 name = os.path.join(*rel)
405 417 auditor(name)
406 418 return util.pconvert(name)
407 419 dirname, basename = util.split(name)
408 420 rel.append(basename)
409 421 if dirname == name:
410 422 break
411 423 name = dirname
412 424
413 425 raise util.Abort('%s not under root' % myname)
414 426
415 427 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
416 428 '''yield every hg repository under path, always recursively.
417 429 The recurse flag will only control recursion into repo working dirs'''
418 430 def errhandler(err):
419 431 if err.filename == path:
420 432 raise err
421 433 samestat = getattr(os.path, 'samestat', None)
422 434 if followsym and samestat is not None:
423 435 def adddir(dirlst, dirname):
424 436 match = False
425 437 dirstat = os.stat(dirname)
426 438 for lstdirstat in dirlst:
427 439 if samestat(dirstat, lstdirstat):
428 440 match = True
429 441 break
430 442 if not match:
431 443 dirlst.append(dirstat)
432 444 return not match
433 445 else:
434 446 followsym = False
435 447
436 448 if (seen_dirs is None) and followsym:
437 449 seen_dirs = []
438 450 adddir(seen_dirs, path)
439 451 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
440 452 dirs.sort()
441 453 if '.hg' in dirs:
442 454 yield root # found a repository
443 455 qroot = os.path.join(root, '.hg', 'patches')
444 456 if os.path.isdir(os.path.join(qroot, '.hg')):
445 457 yield qroot # we have a patch queue repo here
446 458 if recurse:
447 459 # avoid recursing inside the .hg directory
448 460 dirs.remove('.hg')
449 461 else:
450 462 dirs[:] = [] # don't descend further
451 463 elif followsym:
452 464 newdirs = []
453 465 for d in dirs:
454 466 fname = os.path.join(root, d)
455 467 if adddir(seen_dirs, fname):
456 468 if os.path.islink(fname):
457 469 for hgname in walkrepos(fname, True, seen_dirs):
458 470 yield hgname
459 471 else:
460 472 newdirs.append(d)
461 473 dirs[:] = newdirs
462 474
463 475 def osrcpath():
464 476 '''return default os-specific hgrc search path'''
465 477 path = systemrcpath()
466 478 path.extend(userrcpath())
467 479 path = [os.path.normpath(f) for f in path]
468 480 return path
469 481
470 482 _rcpath = None
471 483
472 484 def rcpath():
473 485 '''return hgrc search path. if env var HGRCPATH is set, use it.
474 486 for each item in path, if directory, use files ending in .rc,
475 487 else use item.
476 488 make HGRCPATH empty to only look in .hg/hgrc of current repo.
477 489 if no HGRCPATH, use default os-specific path.'''
478 490 global _rcpath
479 491 if _rcpath is None:
480 492 if 'HGRCPATH' in os.environ:
481 493 _rcpath = []
482 494 for p in os.environ['HGRCPATH'].split(os.pathsep):
483 495 if not p:
484 496 continue
485 497 p = util.expandpath(p)
486 498 if os.path.isdir(p):
487 499 for f, kind in osutil.listdir(p):
488 500 if f.endswith('.rc'):
489 501 _rcpath.append(os.path.join(p, f))
490 502 else:
491 503 _rcpath.append(p)
492 504 else:
493 505 _rcpath = osrcpath()
494 506 return _rcpath
495 507
496 508 if os.name != 'nt':
497 509
498 510 def rcfiles(path):
499 511 rcs = [os.path.join(path, 'hgrc')]
500 512 rcdir = os.path.join(path, 'hgrc.d')
501 513 try:
502 514 rcs.extend([os.path.join(rcdir, f)
503 515 for f, kind in osutil.listdir(rcdir)
504 516 if f.endswith(".rc")])
505 517 except OSError:
506 518 pass
507 519 return rcs
508 520
509 521 def systemrcpath():
510 522 path = []
511 523 if sys.platform == 'plan9':
512 524 root = 'lib/mercurial'
513 525 else:
514 526 root = 'etc/mercurial'
515 527 # old mod_python does not set sys.argv
516 528 if len(getattr(sys, 'argv', [])) > 0:
517 529 p = os.path.dirname(os.path.dirname(sys.argv[0]))
518 530 path.extend(rcfiles(os.path.join(p, root)))
519 531 path.extend(rcfiles('/' + root))
520 532 return path
521 533
522 534 def userrcpath():
523 535 if sys.platform == 'plan9':
524 536 return [os.environ['home'] + '/lib/hgrc']
525 537 else:
526 538 return [os.path.expanduser('~/.hgrc')]
527 539
528 540 else:
529 541
530 542 import _winreg
531 543
532 544 def systemrcpath():
533 545 '''return default os-specific hgrc search path'''
534 546 rcpath = []
535 547 filename = util.executablepath()
536 548 # Use mercurial.ini found in directory with hg.exe
537 549 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
538 550 if os.path.isfile(progrc):
539 551 rcpath.append(progrc)
540 552 return rcpath
541 553 # Use hgrc.d found in directory with hg.exe
542 554 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
543 555 if os.path.isdir(progrcd):
544 556 for f, kind in osutil.listdir(progrcd):
545 557 if f.endswith('.rc'):
546 558 rcpath.append(os.path.join(progrcd, f))
547 559 return rcpath
548 560 # else look for a system rcpath in the registry
549 561 value = util.lookupreg('SOFTWARE\\Mercurial', None,
550 562 _winreg.HKEY_LOCAL_MACHINE)
551 563 if not isinstance(value, str) or not value:
552 564 return rcpath
553 565 value = util.localpath(value)
554 566 for p in value.split(os.pathsep):
555 567 if p.lower().endswith('mercurial.ini'):
556 568 rcpath.append(p)
557 569 elif os.path.isdir(p):
558 570 for f, kind in osutil.listdir(p):
559 571 if f.endswith('.rc'):
560 572 rcpath.append(os.path.join(p, f))
561 573 return rcpath
562 574
563 575 def userrcpath():
564 576 '''return os-specific hgrc search path to the user dir'''
565 577 home = os.path.expanduser('~')
566 578 path = [os.path.join(home, 'mercurial.ini'),
567 579 os.path.join(home, '.hgrc')]
568 580 userprofile = os.environ.get('USERPROFILE')
569 581 if userprofile:
570 582 path.append(os.path.join(userprofile, 'mercurial.ini'))
571 583 path.append(os.path.join(userprofile, '.hgrc'))
572 584 return path
573 585
574 586 def revsingle(repo, revspec, default='.'):
575 587 if not revspec:
576 588 return repo[default]
577 589
578 590 l = revrange(repo, [revspec])
579 591 if len(l) < 1:
580 592 raise util.Abort(_('empty revision set'))
581 593 return repo[l[-1]]
582 594
583 595 def revpair(repo, revs):
584 596 if not revs:
585 597 return repo.dirstate.p1(), None
586 598
587 599 l = revrange(repo, revs)
588 600
589 601 if len(l) == 0:
590 602 if revs:
591 603 raise util.Abort(_('empty revision range'))
592 604 return repo.dirstate.p1(), None
593 605
594 606 if len(l) == 1 and len(revs) == 1 and _revrangesep not in revs[0]:
595 607 return repo.lookup(l[0]), None
596 608
597 609 return repo.lookup(l[0]), repo.lookup(l[-1])
598 610
599 611 _revrangesep = ':'
600 612
601 613 def revrange(repo, revs):
602 614 """Yield revision as strings from a list of revision specifications."""
603 615
604 616 def revfix(repo, val, defval):
605 617 if not val and val != 0 and defval is not None:
606 618 return defval
607 619 return repo[val].rev()
608 620
609 621 seen, l = set(), []
610 622 for spec in revs:
611 623 if l and not seen:
612 624 seen = set(l)
613 625 # attempt to parse old-style ranges first to deal with
614 626 # things like old-tag which contain query metacharacters
615 627 try:
616 628 if isinstance(spec, int):
617 629 seen.add(spec)
618 630 l.append(spec)
619 631 continue
620 632
621 633 if _revrangesep in spec:
622 634 start, end = spec.split(_revrangesep, 1)
623 635 start = revfix(repo, start, 0)
624 636 end = revfix(repo, end, len(repo) - 1)
625 637 step = start > end and -1 or 1
626 638 if not seen and not l:
627 639 # by far the most common case: revs = ["-1:0"]
628 640 l = range(start, end + step, step)
629 641 # defer syncing seen until next iteration
630 642 continue
631 643 newrevs = set(xrange(start, end + step, step))
632 644 if seen:
633 645 newrevs.difference_update(seen)
634 646 seen.update(newrevs)
635 647 else:
636 648 seen = newrevs
637 649 l.extend(sorted(newrevs, reverse=start > end))
638 650 continue
639 651 elif spec and spec in repo: # single unquoted rev
640 652 rev = revfix(repo, spec, None)
641 653 if rev in seen:
642 654 continue
643 655 seen.add(rev)
644 656 l.append(rev)
645 657 continue
646 658 except error.RepoLookupError:
647 659 pass
648 660
649 661 # fall through to new-style queries if old-style fails
650 662 m = revset.match(repo.ui, spec)
651 663 dl = [r for r in m(repo, list(repo)) if r not in seen]
652 664 l.extend(dl)
653 665 seen.update(dl)
654 666
655 667 return l
656 668
657 669 def expandpats(pats):
658 670 if not util.expandglobs:
659 671 return list(pats)
660 672 ret = []
661 673 for p in pats:
662 674 kind, name = matchmod._patsplit(p, None)
663 675 if kind is None:
664 676 try:
665 677 globbed = glob.glob(name)
666 678 except re.error:
667 679 globbed = [name]
668 680 if globbed:
669 681 ret.extend(globbed)
670 682 continue
671 683 ret.append(p)
672 684 return ret
673 685
674 686 def matchandpats(ctx, pats=[], opts={}, globbed=False, default='relpath'):
675 687 if pats == ("",):
676 688 pats = []
677 689 if not globbed and default == 'relpath':
678 690 pats = expandpats(pats or [])
679 691
680 692 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
681 693 default)
682 694 def badfn(f, msg):
683 695 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
684 696 m.bad = badfn
685 697 return m, pats
686 698
687 699 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
688 700 return matchandpats(ctx, pats, opts, globbed, default)[0]
689 701
690 702 def matchall(repo):
691 703 return matchmod.always(repo.root, repo.getcwd())
692 704
693 705 def matchfiles(repo, files):
694 706 return matchmod.exact(repo.root, repo.getcwd(), files)
695 707
696 708 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
697 709 if dry_run is None:
698 710 dry_run = opts.get('dry_run')
699 711 if similarity is None:
700 712 similarity = float(opts.get('similarity') or 0)
701 713 # we'd use status here, except handling of symlinks and ignore is tricky
702 714 added, unknown, deleted, removed = [], [], [], []
703 715 audit_path = pathauditor(repo.root)
704 716 m = match(repo[None], pats, opts)
705 717 rejected = []
706 718 m.bad = lambda x, y: rejected.append(x)
707 719
708 720 for abs in repo.walk(m):
709 721 target = repo.wjoin(abs)
710 722 good = True
711 723 try:
712 724 audit_path(abs)
713 725 except (OSError, util.Abort):
714 726 good = False
715 727 rel = m.rel(abs)
716 728 exact = m.exact(abs)
717 729 if good and abs not in repo.dirstate:
718 730 unknown.append(abs)
719 731 if repo.ui.verbose or not exact:
720 732 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
721 733 elif (repo.dirstate[abs] != 'r' and
722 734 (not good or not os.path.lexists(target) or
723 735 (os.path.isdir(target) and not os.path.islink(target)))):
724 736 deleted.append(abs)
725 737 if repo.ui.verbose or not exact:
726 738 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
727 739 # for finding renames
728 740 elif repo.dirstate[abs] == 'r':
729 741 removed.append(abs)
730 742 elif repo.dirstate[abs] == 'a':
731 743 added.append(abs)
732 744 copies = {}
733 745 if similarity > 0:
734 746 for old, new, score in similar.findrenames(repo,
735 747 added + unknown, removed + deleted, similarity):
736 748 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
737 749 repo.ui.status(_('recording removal of %s as rename to %s '
738 750 '(%d%% similar)\n') %
739 751 (m.rel(old), m.rel(new), score * 100))
740 752 copies[new] = old
741 753
742 754 if not dry_run:
743 755 wctx = repo[None]
744 756 wlock = repo.wlock()
745 757 try:
746 758 wctx.forget(deleted)
747 759 wctx.add(unknown)
748 760 for new, old in copies.iteritems():
749 761 wctx.copy(old, new)
750 762 finally:
751 763 wlock.release()
752 764
753 765 for f in rejected:
754 766 if f in m.files():
755 767 return 1
756 768 return 0
757 769
758 770 def updatedir(ui, repo, patches, similarity=0):
759 771 '''Update dirstate after patch application according to metadata'''
760 772 if not patches:
761 773 return []
762 774 copies = []
763 775 removes = set()
764 776 cfiles = patches.keys()
765 777 cwd = repo.getcwd()
766 778 if cwd:
767 779 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
768 780 for f in patches:
769 781 gp = patches[f]
770 782 if not gp:
771 783 continue
772 784 if gp.op == 'RENAME':
773 785 copies.append((gp.oldpath, gp.path))
774 786 removes.add(gp.oldpath)
775 787 elif gp.op == 'COPY':
776 788 copies.append((gp.oldpath, gp.path))
777 789 elif gp.op == 'DELETE':
778 790 removes.add(gp.path)
779 791
780 792 wctx = repo[None]
781 793 for src, dst in copies:
782 794 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
783 795 if (not similarity) and removes:
784 796 wctx.remove(sorted(removes), True)
785 797
786 798 for f in patches:
787 799 gp = patches[f]
788 800 if gp and gp.mode:
789 801 islink, isexec = gp.mode
790 802 dst = repo.wjoin(gp.path)
791 803 # patch won't create empty files
792 804 if gp.op == 'ADD' and not os.path.lexists(dst):
793 805 flags = (isexec and 'x' or '') + (islink and 'l' or '')
794 806 repo.wwrite(gp.path, '', flags)
795 807 util.setflags(dst, islink, isexec)
796 808 addremove(repo, cfiles, similarity=similarity)
797 809 files = patches.keys()
798 810 files.extend([r for r in removes if r not in files])
799 811 return sorted(files)
800 812
801 813 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
802 814 """Update the dirstate to reflect the intent of copying src to dst. For
803 815 different reasons it might not end with dst being marked as copied from src.
804 816 """
805 817 origsrc = repo.dirstate.copied(src) or src
806 818 if dst == origsrc: # copying back a copy?
807 819 if repo.dirstate[dst] not in 'mn' and not dryrun:
808 820 repo.dirstate.normallookup(dst)
809 821 else:
810 822 if repo.dirstate[origsrc] == 'a' and origsrc == src:
811 823 if not ui.quiet:
812 824 ui.warn(_("%s has not been committed yet, so no copy "
813 825 "data will be stored for %s.\n")
814 826 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
815 827 if repo.dirstate[dst] in '?r' and not dryrun:
816 828 wctx.add([dst])
817 829 elif not dryrun:
818 830 wctx.copy(origsrc, dst)
819 831
820 832 def readrequires(opener, supported):
821 833 '''Reads and parses .hg/requires and checks if all entries found
822 834 are in the list of supported features.'''
823 835 requirements = set(opener.read("requires").splitlines())
824 836 missings = []
825 837 for r in requirements:
826 838 if r not in supported:
827 839 if not r or not r[0].isalnum():
828 840 raise error.RequirementError(_(".hg/requires file is corrupt"))
829 841 missings.append(r)
830 842 missings.sort()
831 843 if missings:
832 844 raise error.RequirementError(
833 845 _("unknown repository format: requires features '%s' (upgrade "
834 846 "Mercurial)") % "', '".join(missings))
835 847 return requirements
836 848
837 849 class filecacheentry(object):
838 850 def __init__(self, path):
839 851 self.path = path
840 852 self.cachestat = filecacheentry.stat(self.path)
841 853
842 854 if self.cachestat:
843 855 self._cacheable = self.cachestat.cacheable()
844 856 else:
845 857 # None means we don't know yet
846 858 self._cacheable = None
847 859
848 860 def refresh(self):
849 861 if self.cacheable():
850 862 self.cachestat = filecacheentry.stat(self.path)
851 863
852 864 def cacheable(self):
853 865 if self._cacheable is not None:
854 866 return self._cacheable
855 867
856 868 # we don't know yet, assume it is for now
857 869 return True
858 870
859 871 def changed(self):
860 872 # no point in going further if we can't cache it
861 873 if not self.cacheable():
862 874 return True
863 875
864 876 newstat = filecacheentry.stat(self.path)
865 877
866 878 # we may not know if it's cacheable yet, check again now
867 879 if newstat and self._cacheable is None:
868 880 self._cacheable = newstat.cacheable()
869 881
870 882 # check again
871 883 if not self._cacheable:
872 884 return True
873 885
874 886 if self.cachestat != newstat:
875 887 self.cachestat = newstat
876 888 return True
877 889 else:
878 890 return False
879 891
880 892 @staticmethod
881 893 def stat(path):
882 894 try:
883 895 return util.cachestat(path)
884 896 except OSError, e:
885 897 if e.errno != errno.ENOENT:
886 898 raise
887 899
888 900 class filecache(object):
889 901 '''A property like decorator that tracks a file under .hg/ for updates.
890 902
891 903 Records stat info when called in _filecache.
892 904
893 905 On subsequent calls, compares old stat info with new info, and recreates
894 906 the object when needed, updating the new stat info in _filecache.
895 907
896 908 Mercurial either atomic renames or appends for files under .hg,
897 909 so to ensure the cache is reliable we need the filesystem to be able
898 910 to tell us if a file has been replaced. If it can't, we fallback to
899 911 recreating the object on every call (essentially the same behaviour as
900 912 propertycache).'''
901 913 def __init__(self, path):
902 914 self.path = path
903 915
904 916 def join(self, obj, fname):
905 917 """Used to compute the runtime path of the cached file.
906 918
907 919 Users should subclass filecache and provide their own version of this
908 920 function to call the appropriate join function on 'obj' (an instance
909 921 of the class that its member function was decorated).
910 922 """
911 923 return obj.join(fname)
912 924
913 925 def __call__(self, func):
914 926 self.func = func
915 927 self.name = func.__name__
916 928 return self
917 929
918 930 def __get__(self, obj, type=None):
919 931 # do we need to check if the file changed?
920 932 if self.name in obj.__dict__:
921 933 return obj.__dict__[self.name]
922 934
923 935 entry = obj._filecache.get(self.name)
924 936
925 937 if entry:
926 938 if entry.changed():
927 939 entry.obj = self.func(obj)
928 940 else:
929 941 path = self.join(obj, self.path)
930 942
931 943 # We stat -before- creating the object so our cache doesn't lie if
932 944 # a writer modified between the time we read and stat
933 945 entry = filecacheentry(path)
934 946 entry.obj = self.func(obj)
935 947
936 948 obj._filecache[self.name] = entry
937 949
938 950 obj.__dict__[self.name] = entry.obj
939 951 return entry.obj
940 952
941 953 def __set__(self, obj, value):
942 954 if self.name in obj._filecache:
943 955 obj._filecache[self.name].obj = value # update cached copy
944 956 obj.__dict__[self.name] = value # update copy returned by obj.x
945 957
946 958 def __delete__(self, obj):
947 959 try:
948 960 del obj.__dict__[self.name]
949 961 except KeyError:
950 962 raise AttributeError, self.name
@@ -1,546 +1,538
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 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 readdir = self.rawvfs.readdir
315 315 while visit:
316 316 p = visit.pop()
317 317 for f, kind, st in readdir(p, stat=True):
318 318 fp = p + '/' + f
319 319 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
320 320 n = util.pconvert(fp[striplen:])
321 321 l.append((decodedir(n), n, st.st_size))
322 322 elif kind == stat.S_IFDIR and recurse:
323 323 visit.append(fp)
324 324 l.sort()
325 325 return l
326 326
327 327 def datafiles(self):
328 328 return self._walk('data', True)
329 329
330 330 def walk(self):
331 331 '''yields (unencoded, encoded, size)'''
332 332 # yield data files first
333 333 for x in self.datafiles():
334 334 yield x
335 335 # yield manifest before changelog
336 336 for x in reversed(self._walk('', False)):
337 337 yield x
338 338
339 339 def copylist(self):
340 340 return ['requires'] + _data.split()
341 341
342 342 def write(self):
343 343 pass
344 344
345 345 def __contains__(self, path):
346 346 '''Checks if the store contains path'''
347 347 path = "/".join(("data", path))
348 348 # file?
349 349 if os.path.exists(self.join(path + ".i")):
350 350 return True
351 351 # dir?
352 352 if not path.endswith("/"):
353 353 path = path + "/"
354 354 return os.path.exists(self.join(path))
355 355
356 356 class encodedstore(basicstore):
357 357 def __init__(self, path, vfstype):
358 358 vfs = vfstype(path + '/store')
359 359 self.path = vfs.base
360 360 self.createmode = _calcmode(vfs)
361 361 vfs.createmode = self.createmode
362 362 self.rawvfs = vfs
363 363 self.vfs = scmutil.filtervfs(vfs, encodefilename)
364 364 self.opener = self.vfs
365 365
366 366 def datafiles(self):
367 367 for a, b, size in self._walk('data', True):
368 368 try:
369 369 a = decodefilename(a)
370 370 except KeyError:
371 371 a = None
372 372 yield a, b, size
373 373
374 374 def join(self, f):
375 375 return self.path + '/' + encodefilename(f)
376 376
377 377 def copylist(self):
378 378 return (['requires', '00changelog.i'] +
379 379 ['store/' + f for f in _data.split()])
380 380
381 381 class fncache(object):
382 382 # the filename used to be partially encoded
383 383 # hence the encodedir/decodedir dance
384 384 def __init__(self, vfs):
385 385 self.vfs = vfs
386 386 self.entries = None
387 387 self._dirty = False
388 388
389 389 def _load(self):
390 390 '''fill the entries from the fncache file'''
391 391 self._dirty = False
392 392 try:
393 393 fp = self.vfs('fncache', mode='rb')
394 394 except IOError:
395 395 # skip nonexistent file
396 396 self.entries = set()
397 397 return
398 398 self.entries = set(decodedir(fp.read()).splitlines())
399 399 if '' in self.entries:
400 400 fp.seek(0)
401 401 for n, line in enumerate(fp):
402 402 if not line.rstrip('\n'):
403 403 t = _('invalid entry in fncache, line %s') % (n + 1)
404 404 raise util.Abort(t)
405 405 fp.close()
406 406
407 407 def _write(self, files, atomictemp):
408 408 fp = self.vfs('fncache', mode='wb', atomictemp=atomictemp)
409 409 if files:
410 410 fp.write(encodedir('\n'.join(files) + '\n'))
411 411 fp.close()
412 412 self._dirty = False
413 413
414 414 def rewrite(self, files):
415 415 self._write(files, False)
416 416 self.entries = set(files)
417 417
418 418 def write(self):
419 419 if self._dirty:
420 420 self._write(self.entries, True)
421 421
422 422 def add(self, fn):
423 423 if self.entries is None:
424 424 self._load()
425 425 if fn not in self.entries:
426 426 self._dirty = True
427 427 self.entries.add(fn)
428 428
429 429 def __contains__(self, fn):
430 430 if self.entries is None:
431 431 self._load()
432 432 return fn in self.entries
433 433
434 434 def __iter__(self):
435 435 if self.entries is None:
436 436 self._load()
437 437 return iter(self.entries)
438 438
439 class _fncachevfs(scmutil.abstractvfs):
439 class _fncachevfs(scmutil.abstractvfs, scmutil.auditvfs):
440 440 def __init__(self, vfs, fnc, encode):
441 self.vfs = vfs
441 scmutil.auditvfs.__init__(self, vfs)
442 442 self.fncache = fnc
443 443 self.encode = encode
444 444
445 def _getmustaudit(self):
446 return self.vfs.mustaudit
447
448 def _setmustaudit(self, onoff):
449 self.vfs.mustaudit = onoff
450
451 mustaudit = property(_getmustaudit, _setmustaudit)
452
453 445 def __call__(self, path, mode='r', *args, **kw):
454 446 if mode not in ('r', 'rb') and path.startswith('data/'):
455 447 self.fncache.add(path)
456 448 return self.vfs(self.encode(path), mode, *args, **kw)
457 449
458 450 def join(self, path):
459 451 if path:
460 452 return self.vfs.join(self.encode(path))
461 453 else:
462 454 return self.vfs.join(path)
463 455
464 456 class fncachestore(basicstore):
465 457 def __init__(self, path, vfstype, dotencode):
466 458 if dotencode:
467 459 encode = _dothybridencode
468 460 else:
469 461 encode = _plainhybridencode
470 462 self.encode = encode
471 463 vfs = vfstype(path + '/store')
472 464 self.path = vfs.base
473 465 self.pathsep = self.path + '/'
474 466 self.createmode = _calcmode(vfs)
475 467 vfs.createmode = self.createmode
476 468 self.rawvfs = vfs
477 469 fnc = fncache(vfs)
478 470 self.fncache = fnc
479 471 self.vfs = _fncachevfs(vfs, fnc, encode)
480 472 self.opener = self.vfs
481 473
482 474 def join(self, f):
483 475 return self.pathsep + self.encode(f)
484 476
485 477 def getsize(self, path):
486 478 return self.rawvfs.stat(path).st_size
487 479
488 480 def datafiles(self):
489 481 rewrite = False
490 482 existing = []
491 483 for f in sorted(self.fncache):
492 484 ef = self.encode(f)
493 485 try:
494 486 yield f, ef, self.getsize(ef)
495 487 existing.append(f)
496 488 except OSError, err:
497 489 if err.errno != errno.ENOENT:
498 490 raise
499 491 # nonexistent entry
500 492 rewrite = True
501 493 if rewrite:
502 494 # rewrite fncache to remove nonexistent entries
503 495 # (may be caused by rollback / strip)
504 496 self.fncache.rewrite(existing)
505 497
506 498 def copylist(self):
507 499 d = ('data dh fncache phaseroots obsstore'
508 500 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
509 501 return (['requires', '00changelog.i'] +
510 502 ['store/' + f for f in d.split()])
511 503
512 504 def write(self):
513 505 self.fncache.write()
514 506
515 507 def _exists(self, f):
516 508 ef = self.encode(f)
517 509 try:
518 510 self.getsize(ef)
519 511 return True
520 512 except OSError, err:
521 513 if err.errno != errno.ENOENT:
522 514 raise
523 515 # nonexistent entry
524 516 return False
525 517
526 518 def __contains__(self, path):
527 519 '''Checks if the store contains path'''
528 520 path = "/".join(("data", path))
529 521 # check for files (exact match)
530 522 e = path + '.i'
531 523 if e in self.fncache and self._exists(e):
532 524 return True
533 525 # now check for directories (prefix match)
534 526 if not path.endswith('/'):
535 527 path += '/'
536 528 for e in self.fncache:
537 529 if e.startswith(path) and self._exists(e):
538 530 return True
539 531 return False
540 532
541 533 def store(requirements, path, vfstype):
542 534 if 'store' in requirements:
543 535 if 'fncache' in requirements:
544 536 return fncachestore(path, vfstype, 'dotencode' in requirements)
545 537 return encodedstore(path, vfstype)
546 538 return basicstore(path, vfstype)
General Comments 0
You need to be logged in to leave comments. Login now