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