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