dirstate.py
1006 lines
| 35.6 KiB
| text/x-python
|
PythonLexer
/ mercurial / dirstate.py
|
r8226 | # dirstate.py - working directory tracking for mercurial | ||
# | ||||
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r1089 | |||
|
r6211 | from node import nullid | ||
|
r3891 | from i18n import _ | ||
|
r25216 | import scmutil, util, osutil, parsers, encoding, pathutil | ||
|
r23496 | import os, stat, errno | ||
|
r25216 | import match as matchmod | ||
|
r1089 | |||
|
r8261 | propertycache = util.propertycache | ||
|
r16201 | filecache = scmutil.filecache | ||
|
r17733 | _rangemask = 0x7fffffff | ||
|
r16201 | |||
|
r21809 | dirstatetuple = parsers.dirstatetuple | ||
|
r21808 | |||
|
r16201 | class repocache(filecache): | ||
"""filecache for files in .hg/""" | ||||
def join(self, obj, fname): | ||||
return obj._opener.join(fname) | ||||
|
r4610 | |||
|
r16202 | class rootcache(filecache): | ||
"""filecache for files in the repository root""" | ||||
def join(self, obj, fname): | ||||
return obj._join(fname) | ||||
|
r1559 | class dirstate(object): | ||
|
r2393 | |||
|
r13032 | def __init__(self, opener, ui, root, validate): | ||
|
r10145 | '''Create a new dirstate object. | ||
opener is an open()-like callable that can be used to open the | ||||
dirstate file; root is the root of the directory tracked by | ||||
the dirstate. | ||||
''' | ||||
|
r4614 | self._opener = opener | ||
|
r13032 | self._validate = validate | ||
|
r4614 | self._root = root | ||
|
r24198 | # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is | ||
# UNC path pointing to root share (issue4557) | ||||
|
r24833 | self._rootdir = pathutil.normasprefix(root) | ||
|
r4903 | self._dirty = False | ||
|
r4965 | self._dirtypl = False | ||
|
r15791 | self._lastnormaltime = 0 | ||
|
r4614 | self._ui = ui | ||
|
r16200 | self._filecache = {} | ||
|
r22404 | self._parentwriters = 0 | ||
|
r25226 | self._filename = 'dirstate' | ||
|
r22404 | |||
def beginparentchange(self): | ||||
'''Marks the beginning of a set of changes that involve changing | ||||
the dirstate parents. If there is an exception during this time, | ||||
the dirstate will not be written when the wlock is released. This | ||||
prevents writing an incoherent dirstate where the parent doesn't | ||||
match the contents. | ||||
''' | ||||
self._parentwriters += 1 | ||||
def endparentchange(self): | ||||
'''Marks the end of a set of changes that involve changing the | ||||
dirstate parents. Once all parent changes have been marked done, | ||||
the wlock will be free to write the dirstate on release. | ||||
''' | ||||
if self._parentwriters > 0: | ||||
self._parentwriters -= 1 | ||||
def pendingparentchange(self): | ||||
'''Returns true if the dirstate is in the middle of a set of changes | ||||
that modify the dirstate parent. | ||||
''' | ||||
return self._parentwriters > 0 | ||||
|
r1089 | |||
|
r8261 | @propertycache | ||
def _map(self): | ||||
|
r9518 | '''Return the dirstate contents as a map from filename to | ||
(state, mode, size, time).''' | ||||
|
r8261 | self._read() | ||
return self._map | ||||
@propertycache | ||||
def _copymap(self): | ||||
self._read() | ||||
return self._copymap | ||||
@propertycache | ||||
|
r24540 | def _filefoldmap(self): | ||
|
r24610 | try: | ||
makefilefoldmap = parsers.make_file_foldmap | ||||
except AttributeError: | ||||
pass | ||||
else: | ||||
return makefilefoldmap(self._map, util.normcasespec, | ||||
util.normcasefallback) | ||||
|
r8261 | f = {} | ||
|
r22782 | normcase = util.normcase | ||
|
r19103 | for name, s in self._map.iteritems(): | ||
if s[0] != 'r': | ||||
|
r22782 | f[normcase(name)] = name | ||
|
r24540 | f['.'] = '.' # prevents useless util.fspath() invocation | ||
return f | ||||
@propertycache | ||||
def _dirfoldmap(self): | ||||
f = {} | ||||
normcase = util.normcase | ||||
|
r16302 | for name in self._dirs: | ||
|
r22782 | f[normcase(name)] = name | ||
|
r8261 | return f | ||
|
r16201 | @repocache('branch') | ||
|
r8261 | def _branch(self): | ||
try: | ||||
|
r14168 | return self._opener.read("branch").strip() or "default" | ||
|
r25660 | except IOError as inst: | ||
|
r15799 | if inst.errno != errno.ENOENT: | ||
raise | ||||
|
r8261 | return "default" | ||
@propertycache | ||||
def _pl(self): | ||||
try: | ||||
|
r25226 | fp = self._opener(self._filename) | ||
|
r13400 | st = fp.read(40) | ||
fp.close() | ||||
|
r8716 | l = len(st) | ||
if l == 40: | ||||
|
r8261 | return st[:20], st[20:40] | ||
|
r8716 | elif l > 0 and l < 40: | ||
|
r8640 | raise util.Abort(_('working directory state appears damaged!')) | ||
|
r25660 | except IOError as err: | ||
|
r10282 | if err.errno != errno.ENOENT: | ||
raise | ||||
|
r8261 | return [nullid, nullid] | ||
@propertycache | ||||
def _dirs(self): | ||||
|
r24635 | return util.dirs(self._map, 'r') | ||
|
r8261 | |||
|
r16143 | def dirs(self): | ||
return self._dirs | ||||
|
r16202 | @rootcache('.hgignore') | ||
|
r8261 | def _ignore(self): | ||
|
r25163 | files = [] | ||
if os.path.exists(self._join('.hgignore')): | ||||
files.append(self._join('.hgignore')) | ||||
|
r8261 | for name, path in self._ui.configitems("ui"): | ||
if name == 'ignore' or name.startswith('ignore.'): | ||||
|
r23629 | # we need to use os.path.join here rather than self._join | ||
# because path is arbitrary and user-specified | ||||
files.append(os.path.join(self._rootdir, util.expandpath(path))) | ||||
|
r25216 | |||
if not files: | ||||
return util.never | ||||
pats = ['include:%s' % f for f in files] | ||||
return matchmod.match(self._root, '', [], pats, warn=self._ui.warn) | ||||
|
r8261 | |||
@propertycache | ||||
def _slash(self): | ||||
return self._ui.configbool('ui', 'slash') and os.sep != '/' | ||||
@propertycache | ||||
def _checklink(self): | ||||
return util.checklink(self._root) | ||||
@propertycache | ||||
def _checkexec(self): | ||||
return util.checkexec(self._root) | ||||
@propertycache | ||||
def _checkcase(self): | ||||
return not util.checkcase(self._join('.hg')) | ||||
|
r4905 | def _join(self, f): | ||
|
r6972 | # much faster than os.path.join() | ||
|
r6973 | # it's safe because f is always a relative path | ||
|
r6972 | return self._rootdir + f | ||
|
r1089 | |||
|
r15337 | def flagfunc(self, buildfallback): | ||
if self._checklink and self._checkexec: | ||||
def f(x): | ||||
|
r18869 | try: | ||
st = os.lstat(self._join(x)) | ||||
if util.statislink(st): | ||||
return 'l' | ||||
if util.statisexec(st): | ||||
return 'x' | ||||
except OSError: | ||||
pass | ||||
|
r15337 | return '' | ||
return f | ||||
fallback = buildfallback() | ||||
|
r6743 | if self._checklink: | ||
def f(x): | ||||
|
r6972 | if os.path.islink(self._join(x)): | ||
|
r6743 | return 'l' | ||
if 'x' in fallback(x): | ||||
return 'x' | ||||
return '' | ||||
return f | ||||
if self._checkexec: | ||||
def f(x): | ||||
if 'l' in fallback(x): | ||||
return 'l' | ||||
|
r14273 | if util.isexec(self._join(x)): | ||
|
r6743 | return 'x' | ||
return '' | ||||
return f | ||||
|
r15337 | else: | ||
return fallback | ||||
|
r6743 | |||
|
r20335 | @propertycache | ||
def _cwd(self): | ||||
return os.getcwd() | ||||
|
r1089 | def getcwd(self): | ||
|
r20335 | cwd = self._cwd | ||
|
r10282 | if cwd == self._root: | ||
return '' | ||||
|
r4614 | # self._root ends with a path separator if self._root is '/' or 'C:\' | ||
rootsep = self._root | ||||
|
r5843 | if not util.endswithsep(rootsep): | ||
|
r4230 | rootsep += os.sep | ||
if cwd.startswith(rootsep): | ||||
return cwd[len(rootsep):] | ||||
else: | ||||
# we're outside the repo. return an absolute path. | ||||
return cwd | ||||
|
r1089 | |||
|
r4525 | def pathto(self, f, cwd=None): | ||
if cwd is None: | ||||
cwd = self.getcwd() | ||||
|
r4614 | path = util.pathto(self._root, cwd, f) | ||
|
r4527 | if self._slash: | ||
|
r19210 | return util.pconvert(path) | ||
|
r4527 | return path | ||
|
r4525 | |||
|
r1089 | def __getitem__(self, key): | ||
|
r9518 | '''Return the current state of key (a filename) in the dirstate. | ||
|
r10145 | |||
|
r9518 | States are: | ||
n normal | ||||
m needs merging | ||||
r marked for removal | ||||
a marked for addition | ||||
? not tracked | ||||
''' | ||||
|
r4906 | return self._map.get(key, ("?",))[0] | ||
|
r1089 | |||
def __contains__(self, key): | ||||
|
r4614 | return key in self._map | ||
def __iter__(self): | ||||
|
r8209 | for x in sorted(self._map): | ||
|
r4614 | yield x | ||
|
r1089 | |||
|
r18792 | def iteritems(self): | ||
return self._map.iteritems() | ||||
|
r1089 | def parents(self): | ||
|
r13032 | return [self._validate(p) for p in self._pl] | ||
|
r1089 | |||
|
r13876 | def p1(self): | ||
return self._validate(self._pl[0]) | ||||
def p2(self): | ||||
return self._validate(self._pl[1]) | ||||
|
r4179 | def branch(self): | ||
|
r13047 | return encoding.tolocal(self._branch) | ||
|
r4179 | |||
|
r1089 | def setparents(self, p1, p2=nullid): | ||
|
r16551 | """Set dirstate parents to p1 and p2. | ||
When moving from two parents to one, 'm' merged entries a | ||||
adjusted to normal and previous copy records discarded and | ||||
returned by the call. | ||||
See localrepo.setparents() | ||||
""" | ||||
|
r22407 | if self._parentwriters == 0: | ||
|
r22459 | raise ValueError("cannot set dirstate parent without " | ||
"calling dirstate.beginparentchange") | ||||
|
r22407 | |||
|
r4965 | self._dirty = self._dirtypl = True | ||
|
r16509 | oldp2 = self._pl[1] | ||
|
r4614 | self._pl = p1, p2 | ||
|
r16551 | copies = {} | ||
|
r16509 | if oldp2 != nullid and p2 == nullid: | ||
for f, s in self._map.iteritems(): | ||||
|
r22895 | # Discard 'm' markers when moving away from a merge state | ||
|
r16509 | if s[0] == 'm': | ||
|
r16551 | if f in self._copymap: | ||
copies[f] = self._copymap[f] | ||||
|
r16509 | self.normallookup(f) | ||
|
r22895 | # Also fix up otherparent markers | ||
elif s[0] == 'n' and s[2] == -2: | ||||
if f in self._copymap: | ||||
copies[f] = self._copymap[f] | ||||
self.add(f) | ||||
|
r16551 | return copies | ||
|
r1089 | |||
|
r4179 | def setbranch(self, branch): | ||
|
r13047 | self._branch = encoding.fromlocal(branch) | ||
|
r16472 | f = self._opener('branch', 'w', atomictemp=True) | ||
try: | ||||
f.write(self._branch + '\n') | ||||
f.close() | ||||
|
r18317 | |||
# make sure filecache has the correct stat info for _branch after | ||||
# replacing the underlying file | ||||
ce = self._filecache['_branch'] | ||||
if ce: | ||||
ce.refresh() | ||||
|
r18076 | except: # re-raises | ||
f.discard() | ||||
raise | ||||
|
r4179 | |||
|
r4615 | def _read(self): | ||
|
r4614 | self._map = {} | ||
self._copymap = {} | ||||
|
r4607 | try: | ||
|
r25227 | fp = self._opener.open(self._filename) | ||
try: | ||||
st = fp.read() | ||||
finally: | ||||
fp.close() | ||||
|
r25660 | except IOError as err: | ||
|
r10282 | if err.errno != errno.ENOENT: | ||
raise | ||||
|
r4607 | return | ||
if not st: | ||||
return | ||||
|
r25585 | if util.safehasattr(parsers, 'dict_new_presized'): | ||
# Make an estimate of the number of files in the dirstate based on | ||||
# its size. From a linear regression on a set of real-world repos, | ||||
# all over 10,000 files, the size of a dirstate entry is 85 | ||||
# bytes. The cost of resizing is significantly higher than the cost | ||||
# of filling in a larger presized dict, so subtract 20% from the | ||||
# size. | ||||
# | ||||
# This heuristic is imperfect in many ways, so in a future dirstate | ||||
# format update it makes sense to just record the number of entries | ||||
# on write. | ||||
self._map = parsers.dict_new_presized(len(st) / 71) | ||||
|
r18649 | # Python's garbage collector triggers a GC each time a certain number | ||
# of container objects (the number being defined by | ||||
# gc.get_threshold()) are allocated. parse_dirstate creates a tuple | ||||
# for each file in the dirstate. The C version then immediately marks | ||||
# them as not to be tracked by the collector. However, this has no | ||||
# effect on when GCs are triggered, only on what objects the GC looks | ||||
# into. This means that O(number of files) GCs are unavoidable. | ||||
# Depending on when in the process's lifetime the dirstate is parsed, | ||||
# this can get very expensive. As a workaround, disable GC while | ||||
# parsing the dirstate. | ||||
|
r23496 | # | ||
# (we cannot decorate the function directly since it is in a C module) | ||||
parse_dirstate = util.nogc(parsers.parse_dirstate) | ||||
p = parse_dirstate(self._map, self._copymap, st) | ||||
|
r4952 | if not self._dirtypl: | ||
|
r7093 | self._pl = p | ||
|
r1089 | |||
|
r4613 | def invalidate(self): | ||
|
r24540 | for a in ("_map", "_copymap", "_filefoldmap", "_dirfoldmap", "_branch", | ||
"_pl", "_dirs", "_ignore"): | ||||
|
r4953 | if a in self.__dict__: | ||
delattr(self, a) | ||||
|
r15791 | self._lastnormaltime = 0 | ||
|
r4903 | self._dirty = False | ||
|
r22404 | self._parentwriters = 0 | ||
|
r4375 | |||
|
r1089 | def copy(self, source, dest): | ||
|
r10145 | """Mark dest as a copy of source. Unmark dest if source is None.""" | ||
|
r6680 | if source == dest: | ||
return | ||||
|
r4903 | self._dirty = True | ||
|
r7566 | if source is not None: | ||
self._copymap[dest] = source | ||||
elif dest in self._copymap: | ||||
del self._copymap[dest] | ||||
|
r1089 | |||
def copied(self, file): | ||||
|
r4614 | return self._copymap.get(file, None) | ||
|
r3154 | |||
def copies(self): | ||||
|
r4614 | return self._copymap | ||
|
r1089 | |||
|
r6767 | def _droppath(self, f): | ||
if self[f] not in "?r" and "_dirs" in self.__dict__: | ||||
|
r18899 | self._dirs.delpath(f) | ||
|
r2953 | |||
|
r17196 | def _addpath(self, f, state, mode, size, mtime): | ||
|
r5487 | oldstate = self[f] | ||
|
r17196 | if state == 'a' or oldstate == 'r': | ||
|
r13974 | scmutil.checkfilename(f) | ||
|
r6767 | if f in self._dirs: | ||
raise util.Abort(_('directory %r already in dirstate') % f) | ||||
# shadows | ||||
|
r24635 | for d in util.finddirs(f): | ||
|
r6767 | if d in self._dirs: | ||
break | ||||
if d in self._map and self[d] != 'r': | ||||
raise util.Abort( | ||||
_('file %r in dirstate clashes with %r') % (d, f)) | ||||
if oldstate in "?r" and "_dirs" in self.__dict__: | ||||
|
r18899 | self._dirs.addpath(f) | ||
|
r17094 | self._dirty = True | ||
|
r21808 | self._map[f] = dirstatetuple(state, mode, size, mtime) | ||
|
r5487 | |||
|
r4904 | def normal(self, f): | ||
|
r10145 | '''Mark a file normal and clean.''' | ||
|
r4905 | s = os.lstat(self._join(f)) | ||
|
r13754 | mtime = int(s.st_mtime) | ||
|
r17733 | self._addpath(f, 'n', s.st_mode, | ||
s.st_size & _rangemask, mtime & _rangemask) | ||||
|
r5915 | if f in self._copymap: | ||
|
r4904 | del self._copymap[f] | ||
|
r13763 | if mtime > self._lastnormaltime: | ||
# Remember the most recent modification timeslot for status(), | ||||
|
r13754 | # to make sure we won't miss future size-preserving file content | ||
# modifications that happen within the same timeslot. | ||||
|
r13763 | self._lastnormaltime = mtime | ||
|
r13704 | |||
|
r5210 | def normallookup(self, f): | ||
|
r10145 | '''Mark a file normal, but possibly dirty.''' | ||
|
r6298 | if self._pl[1] != nullid and f in self._map: | ||
# if there is a merge going on and the file was either | ||||
|
r10968 | # in state 'm' (-1) or coming from other parent (-2) before | ||
# being removed, restore that state. | ||||
|
r6298 | entry = self._map[f] | ||
if entry[0] == 'r' and entry[2] in (-1, -2): | ||||
source = self._copymap.get(f) | ||||
if entry[2] == -1: | ||||
self.merge(f) | ||||
elif entry[2] == -2: | ||||
|
r10968 | self.otherparent(f) | ||
|
r6298 | if source: | ||
self.copy(source, f) | ||||
return | ||||
if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2: | ||||
return | ||||
|
r17094 | self._addpath(f, 'n', 0, -1, -1) | ||
|
r5210 | if f in self._copymap: | ||
del self._copymap[f] | ||||
|
r10968 | def otherparent(self, f): | ||
'''Mark as coming from the other parent, always dirty.''' | ||||
if self._pl[1] == nullid: | ||||
raise util.Abort(_("setting %r to other parent " | ||||
"only allowed in merges") % f) | ||||
|
r22896 | if f in self and self[f] == 'n': | ||
# merge-like | ||||
self._addpath(f, 'm', 0, -2, -1) | ||||
else: | ||||
# add-like | ||||
self._addpath(f, 'n', 0, -2, -1) | ||||
|
r4904 | if f in self._copymap: | ||
del self._copymap[f] | ||||
def add(self, f): | ||||
|
r10145 | '''Mark a file added.''' | ||
|
r17196 | self._addpath(f, 'a', 0, -1, -1) | ||
|
r4904 | if f in self._copymap: | ||
del self._copymap[f] | ||||
|
r4616 | |||
|
r4904 | def remove(self, f): | ||
|
r10145 | '''Mark a file removed.''' | ||
|
r4904 | self._dirty = True | ||
|
r6767 | self._droppath(f) | ||
|
r6297 | size = 0 | ||
if self._pl[1] != nullid and f in self._map: | ||||
|
r10968 | # backup the previous state | ||
|
r6297 | entry = self._map[f] | ||
|
r10968 | if entry[0] == 'm': # merge | ||
|
r6297 | size = -1 | ||
|
r10968 | elif entry[0] == 'n' and entry[2] == -2: # other parent | ||
|
r6297 | size = -2 | ||
|
r21808 | self._map[f] = dirstatetuple('r', 0, size, 0) | ||
|
r6297 | if size == 0 and f in self._copymap: | ||
|
r4904 | del self._copymap[f] | ||
|
r1089 | |||
|
r4904 | def merge(self, f): | ||
|
r10145 | '''Mark a file merged.''' | ||
|
r16509 | if self._pl[1] == nullid: | ||
return self.normallookup(f) | ||||
|
r22897 | return self.otherparent(f) | ||
|
r4904 | |||
|
r14434 | def drop(self, f): | ||
'''Drop a file from the dirstate''' | ||||
|
r15399 | if f in self._map: | ||
self._dirty = True | ||||
self._droppath(f) | ||||
del self._map[f] | ||||
|
r1089 | |||
|
r24538 | def _discoverpath(self, path, normed, ignoremissing, exists, storemap): | ||
if exists is None: | ||||
exists = os.path.lexists(os.path.join(self._root, path)) | ||||
if not exists: | ||||
# Maybe a path component exists | ||||
if not ignoremissing and '/' in path: | ||||
d, f = path.rsplit('/', 1) | ||||
d = self._normalize(d, False, ignoremissing, None) | ||||
folded = d + "/" + f | ||||
else: | ||||
# No path components, preserve original case | ||||
folded = path | ||||
else: | ||||
# recursively normalize leading directory components | ||||
# against dirstate | ||||
if '/' in normed: | ||||
d, f = normed.rsplit('/', 1) | ||||
d = self._normalize(d, False, ignoremissing, True) | ||||
r = self._root + "/" + d | ||||
folded = d + "/" + util.fspath(f, r) | ||||
else: | ||||
folded = util.fspath(normed, self._root) | ||||
storemap[normed] = folded | ||||
return folded | ||||
|
r24539 | def _normalizefile(self, path, isknown, ignoremissing=False, exists=None): | ||
|
r15488 | normed = util.normcase(path) | ||
|
r24540 | folded = self._filefoldmap.get(normed, None) | ||
|
r13717 | if folded is None: | ||
|
r16542 | if isknown: | ||
|
r13717 | folded = path | ||
|
r7068 | else: | ||
|
r24539 | folded = self._discoverpath(path, normed, ignoremissing, exists, | ||
|
r24540 | self._filefoldmap) | ||
|
r24539 | return folded | ||
|
r16302 | |||
|
r16542 | def _normalize(self, path, isknown, ignoremissing=False, exists=None): | ||
|
r15488 | normed = util.normcase(path) | ||
|
r24561 | folded = self._filefoldmap.get(normed, None) | ||
if folded is None: | ||||
folded = self._dirfoldmap.get(normed, None) | ||||
|
r13717 | if folded is None: | ||
|
r16542 | if isknown: | ||
|
r13717 | folded = path | ||
|
r7068 | else: | ||
|
r24540 | # store discovered result in dirfoldmap so that future | ||
# normalizefile calls don't start matching directories | ||||
|
r24538 | folded = self._discoverpath(path, normed, ignoremissing, exists, | ||
|
r24540 | self._dirfoldmap) | ||
|
r13717 | return folded | ||
|
r16542 | def normalize(self, path, isknown=False, ignoremissing=False): | ||
|
r13717 | ''' | ||
normalize the case of a pathname when on a casefolding filesystem | ||||
isknown specifies whether the filename came from walking the | ||||
|
r16542 | disk, to avoid extra filesystem access. | ||
If ignoremissing is True, missing path are returned | ||||
unchanged. Otherwise, we try harder to normalize possibly | ||||
existing path components. | ||||
|
r13717 | |||
The normalized case is determined based on the following precedence: | ||||
- version of name already stored in the dirstate | ||||
- version of name stored on disk | ||||
- version provided via command arguments | ||||
''' | ||||
if self._checkcase: | ||||
|
r16542 | return self._normalize(path, isknown, ignoremissing) | ||
|
r13717 | return path | ||
|
r6677 | |||
|
r5065 | def clear(self): | ||
self._map = {} | ||||
|
r5487 | if "_dirs" in self.__dict__: | ||
|
r10394 | delattr(self, "_dirs") | ||
|
r5065 | self._copymap = {} | ||
self._pl = [nullid, nullid] | ||||
|
r15791 | self._lastnormaltime = 0 | ||
|
r5123 | self._dirty = True | ||
|
r5065 | |||
|
r18760 | def rebuild(self, parent, allfiles, changedfiles=None): | ||
|
r25448 | if changedfiles is None: | ||
changedfiles = allfiles | ||||
|
r18760 | oldmap = self._map | ||
|
r5065 | self.clear() | ||
|
r18760 | for f in allfiles: | ||
if f not in changedfiles: | ||||
self._map[f] = oldmap[f] | ||||
|
r1755 | else: | ||
|
r18760 | if 'x' in allfiles.flags(f): | ||
|
r25658 | self._map[f] = dirstatetuple('n', 0o777, -1, 0) | ||
|
r18760 | else: | ||
|
r25658 | self._map[f] = dirstatetuple('n', 0o666, -1, 0) | ||
|
r4614 | self._pl = (parent, nullid) | ||
|
r4903 | self._dirty = True | ||
|
r1089 | |||
def write(self): | ||||
|
r4612 | if not self._dirty: | ||
|
r1794 | return | ||
|
r21931 | |||
# enough 'delaywrite' prevents 'pack_dirstate' from dropping | ||||
# timestamp of each entries in dirstate, because of 'now > mtime' | ||||
delaywrite = self._ui.configint('debug', 'dirstate.delaywrite', 0) | ||||
|
r23866 | if delaywrite > 0: | ||
|
r21931 | import time # to avoid useless import | ||
time.sleep(delaywrite) | ||||
|
r25226 | st = self._opener(self._filename, "w", atomictemp=True) | ||
|
r9509 | # use the modification time of the newly created temporary file as the | ||
# filesystem's notion of 'now' | ||||
|
r16955 | now = util.fstat(st).st_mtime | ||
|
r21026 | st.write(parsers.pack_dirstate(self._map, self._copymap, self._pl, now)) | ||
st.close() | ||||
self._lastnormaltime = 0 | ||||
self._dirty = self._dirtypl = False | ||||
|
r1089 | |||
|
r6032 | def _dirignore(self, f): | ||
|
r6479 | if f == '.': | ||
return False | ||||
|
r6032 | if self._ignore(f): | ||
return True | ||||
|
r24635 | for p in util.finddirs(f): | ||
|
r6767 | if self._ignore(p): | ||
|
r6032 | return True | ||
return False | ||||
|
r19173 | def _walkexplicit(self, match, subrepos): | ||
'''Get stat data about the files explicitly specified by match. | ||||
|
r3529 | |||
|
r19174 | Return a triple (results, dirsfound, dirsnotfound). | ||
|
r19173 | - results is a mapping from filename to stat result. It also contains | ||
listings mapping subrepos and .hg to None. | ||||
|
r19174 | - dirsfound is a list of files found to be directories. | ||
|
r19173 | - dirsnotfound is a list of files that the dirstate thinks are | ||
directories and that were not found.''' | ||||
|
r6578 | |||
|
r8681 | def badtype(mode): | ||
|
r8310 | kind = _('unknown') | ||
|
r10282 | if stat.S_ISCHR(mode): | ||
kind = _('character device') | ||||
elif stat.S_ISBLK(mode): | ||||
kind = _('block device') | ||||
elif stat.S_ISFIFO(mode): | ||||
kind = _('fifo') | ||||
elif stat.S_ISSOCK(mode): | ||||
kind = _('socket') | ||||
elif stat.S_ISDIR(mode): | ||||
kind = _('directory') | ||||
|
r8681 | return _('unsupported file type (type is %s)') % kind | ||
|
r6830 | |||
|
r19142 | matchedir = match.explicitdir | ||
|
r8676 | badfn = match.bad | ||
|
r6831 | dmap = self._map | ||
|
r5000 | lstat = os.lstat | ||
|
r6830 | getkind = stat.S_IFMT | ||
|
r6828 | dirkind = stat.S_IFDIR | ||
|
r6830 | regkind = stat.S_IFREG | ||
lnkkind = stat.S_IFLNK | ||||
|
r6831 | join = self._join | ||
|
r19174 | dirsfound = [] | ||
foundadd = dirsfound.append | ||||
|
r19173 | dirsnotfound = [] | ||
|
r19175 | notfoundadd = dirsnotfound.append | ||
|
r6820 | |||
|
r24448 | if not match.isexact() and self._checkcase: | ||
|
r12907 | normalize = self._normalize | ||
else: | ||||
|
r18032 | normalize = None | ||
|
r12907 | |||
|
r12211 | files = sorted(match.files()) | ||
subrepos.sort() | ||||
i, j = 0, 0 | ||||
while i < len(files) and j < len(subrepos): | ||||
subpath = subrepos[j] + "/" | ||||
|
r13233 | if files[i] < subpath: | ||
|
r12211 | i += 1 | ||
continue | ||||
|
r13339 | while i < len(files) and files[i].startswith(subpath): | ||
|
r12211 | del files[i] | ||
j += 1 | ||||
|
r6831 | if not files or '.' in files: | ||
|
r24521 | files = ['.'] | ||
|
r10176 | results = dict.fromkeys(subrepos) | ||
results['.hg'] = None | ||||
|
r5000 | |||
|
r23375 | alldirs = None | ||
|
r12211 | for ff in files: | ||
|
r24523 | # constructing the foldmap is expensive, so don't do it for the | ||
# common case where files is ['.'] | ||||
if normalize and ff != '.': | ||||
|
r24522 | nf = normalize(ff, False, True) | ||
|
r18032 | else: | ||
|
r24522 | nf = ff | ||
|
r6829 | if nf in results: | ||
|
r6820 | continue | ||
try: | ||||
|
r6831 | st = lstat(join(nf)) | ||
|
r6830 | kind = getkind(st.st_mode) | ||
if kind == dirkind: | ||||
|
r8588 | if nf in dmap: | ||
|
r21115 | # file replaced by dir on disk but still in dirstate | ||
|
r8588 | results[nf] = None | ||
|
r19143 | if matchedir: | ||
matchedir(nf) | ||||
|
r24537 | foundadd((nf, ff)) | ||
|
r12401 | elif kind == regkind or kind == lnkkind: | ||
|
r6830 | results[nf] = st | ||
|
r6828 | else: | ||
|
r8681 | badfn(ff, badtype(kind)) | ||
|
r6830 | if nf in dmap: | ||
|
r6829 | results[nf] = None | ||
|
r25660 | except OSError as inst: # nf not found on disk - it is dirstate only | ||
|
r21115 | if nf in dmap: # does it exactly match a missing file? | ||
|
r8675 | results[nf] = None | ||
|
r21115 | else: # does it match a missing directory? | ||
|
r23375 | if alldirs is None: | ||
|
r24635 | alldirs = util.dirs(dmap) | ||
|
r23375 | if nf in alldirs: | ||
if matchedir: | ||||
matchedir(nf) | ||||
notfoundadd(nf) | ||||
|
r8677 | else: | ||
|
r8680 | badfn(ff, inst.strerror) | ||
|
r6820 | |||
|
r19174 | return results, dirsfound, dirsnotfound | ||
|
r19173 | |||
|
r19190 | def walk(self, match, subrepos, unknown, ignored, full=True): | ||
|
r19173 | ''' | ||
Walk recursively through the directory tree, finding all files | ||||
matched by match. | ||||
|
r19190 | If full is False, maybe skip some known-clean files. | ||
|
r19173 | Return a dict mapping filename to stat-like object (either | ||
mercurial.osutil.stat instance or return value of os.stat()). | ||||
|
r19190 | |||
|
r19173 | ''' | ||
|
r19190 | # full is a flag that extensions that hook into walk can use -- this | ||
# implementation doesn't use it at all. This satisfies the contract | ||||
# because we only guarantee a "maybe". | ||||
|
r19173 | |||
if ignored: | ||||
ignore = util.never | ||||
dirignore = util.never | ||||
|
r21115 | elif unknown: | ||
ignore = self._ignore | ||||
dirignore = self._dirignore | ||||
else: | ||||
# if not unknown and not ignored, drop dir recursion and step 2 | ||||
|
r19173 | ignore = util.always | ||
dirignore = util.always | ||||
matchfn = match.matchfn | ||||
matchalways = match.always() | ||||
matchtdir = match.traversedir | ||||
dmap = self._map | ||||
listdir = osutil.listdir | ||||
lstat = os.lstat | ||||
dirkind = stat.S_IFDIR | ||||
regkind = stat.S_IFREG | ||||
lnkkind = stat.S_IFLNK | ||||
join = self._join | ||||
exact = skipstep3 = False | ||||
|
r24448 | if match.isexact(): # match.exact | ||
|
r19173 | exact = True | ||
dirignore = util.always # skip step 2 | ||||
|
r25234 | elif match.prefix(): # match.match, no patterns | ||
|
r19173 | skipstep3 = True | ||
if not exact and self._checkcase: | ||||
normalize = self._normalize | ||||
|
r24541 | normalizefile = self._normalizefile | ||
|
r19173 | skipstep3 = False | ||
else: | ||||
|
r24560 | normalize = self._normalize | ||
|
r24541 | normalizefile = None | ||
|
r19173 | |||
# step 1: find all explicit files | ||||
results, work, dirsnotfound = self._walkexplicit(match, subrepos) | ||||
|
r19172 | skipstep3 = skipstep3 and not (work or dirsnotfound) | ||
|
r24537 | work = [d for d in work if not dirignore(d[0])] | ||
|
r19171 | |||
|
r6826 | # step 2: visit subdirectories | ||
|
r24560 | def traverse(work, alreadynormed): | ||
|
r24559 | wadd = work.append | ||
while work: | ||||
|
r24560 | nd = work.pop() | ||
|
r24559 | skip = None | ||
if nd == '.': | ||||
nd = '' | ||||
|
r18032 | else: | ||
|
r24559 | skip = '.hg' | ||
try: | ||||
entries = listdir(join(nd), stat=True, skip=skip) | ||||
|
r25660 | except OSError as inst: | ||
|
r24559 | if inst.errno in (errno.EACCES, errno.ENOENT): | ||
match.bad(self.pathto(nd), inst.strerror) | ||||
continue | ||||
raise | ||||
for f, kind, st in entries: | ||||
if normalizefile: | ||||
# even though f might be a directory, we're only | ||||
# interested in comparing it to files currently in the | ||||
# dmap -- therefore normalizefile is enough | ||||
nf = normalizefile(nd and (nd + "/" + f) or f, True, | ||||
True) | ||||
else: | ||||
nf = nd and (nd + "/" + f) or f | ||||
if nf not in results: | ||||
if kind == dirkind: | ||||
if not ignore(nf): | ||||
if matchtdir: | ||||
matchtdir(nf) | ||||
|
r24560 | wadd(nf) | ||
|
r24559 | if nf in dmap and (matchalways or matchfn(nf)): | ||
results[nf] = None | ||||
elif kind == regkind or kind == lnkkind: | ||||
if nf in dmap: | ||||
if matchalways or matchfn(nf): | ||||
results[nf] = st | ||||
|
r24560 | elif ((matchalways or matchfn(nf)) | ||
and not ignore(nf)): | ||||
# unknown file -- normalize if necessary | ||||
if not alreadynormed: | ||||
nf = normalize(nf, False, True) | ||||
|
r24559 | results[nf] = st | ||
elif nf in dmap and (matchalways or matchfn(nf)): | ||||
|
r6829 | results[nf] = None | ||
|
r24559 | |||
|
r24560 | for nd, d in work: | ||
# alreadynormed means that processwork doesn't have to do any | ||||
# expensive directory normalization | ||||
alreadynormed = not normalize or nd == d | ||||
traverse([d], alreadynormed) | ||||
|
r1089 | |||
|
r18812 | for s in subrepos: | ||
del results[s] | ||||
del results['.hg'] | ||||
|
r21115 | # step 3: visit remaining files from dmap | ||
|
r8684 | if not skipstep3 and not exact: | ||
|
r21115 | # If a dmap file is not in results yet, it was either | ||
# a) not matching matchfn b) ignored, c) missing, or d) under a | ||||
# symlink directory. | ||||
|
r18815 | if not results and matchalways: | ||
visit = dmap.keys() | ||||
else: | ||||
visit = [f for f in dmap if f not in results and matchfn(f)] | ||||
visit.sort() | ||||
|
r18625 | if unknown: | ||
|
r21115 | # unknown == True means we walked all dirs under the roots | ||
# that wasn't ignored, and everything that matched was stat'ed | ||||
# and is already in results. | ||||
# The rest must thus be ignored or under a symlink. | ||||
|
r20033 | audit_path = pathutil.pathauditor(self._root) | ||
|
r18625 | |||
for nf in iter(visit): | ||||
|
r24621 | # If a stat for the same file was already added with a | ||
# different case, don't add one for this, since that would | ||||
# make it appear as if the file exists under both names | ||||
# on disk. | ||||
|
r24632 | if (normalizefile and | ||
normalizefile(nf, True, True) in results): | ||||
|
r24621 | results[nf] = None | ||
|
r18625 | # Report ignored items in the dmap as long as they are not | ||
# under a symlink directory. | ||||
|
r24621 | elif audit_path.check(nf): | ||
|
r18663 | try: | ||
results[nf] = lstat(join(nf)) | ||||
|
r21115 | # file was just ignored, no links, and exists | ||
|
r18663 | except OSError: | ||
# file doesn't exist | ||||
results[nf] = None | ||||
|
r18625 | else: | ||
# It's either missing or under a symlink directory | ||||
|
r21115 | # which we in this case report as missing | ||
|
r18625 | results[nf] = None | ||
else: | ||||
# We may not have walked the full directory tree above, | ||||
|
r21115 | # so stat and check everything we missed. | ||
|
r18625 | nf = iter(visit).next | ||
for st in util.statfiles([join(i) for i in visit]): | ||||
results[nf()] = st | ||||
|
r6829 | return results | ||
|
r1089 | |||
|
r10176 | def status(self, match, subrepos, ignored, clean, unknown): | ||
|
r9518 | '''Determine the status of the working copy relative to the | ||
|
r22915 | dirstate and return a pair of (unsure, status), where status is of type | ||
scmutil.status and: | ||||
|
r9518 | |||
unsure: | ||||
files that might have been modified since the dirstate was | ||||
written, but need to be read to be sure (size is the same | ||||
but mtime differs) | ||||
|
r22915 | status.modified: | ||
|
r9518 | files that have definitely been modified since the dirstate | ||
was written (different size or mode) | ||||
|
r22915 | status.clean: | ||
|
r9518 | files that have definitely not been modified since the | ||
dirstate was written | ||||
''' | ||||
|
r6753 | listignored, listclean, listunknown = ignored, clean, unknown | ||
|
r2022 | lookup, modified, added, unknown, ignored = [], [], [], [], [] | ||
|
r2661 | removed, deleted, clean = [], [], [] | ||
|
r1089 | |||
|
r5003 | dmap = self._map | ||
|
r9518 | ladd = lookup.append # aka "unsure" | ||
|
r5003 | madd = modified.append | ||
aadd = added.append | ||||
uadd = unknown.append | ||||
iadd = ignored.append | ||||
radd = removed.append | ||||
dadd = deleted.append | ||||
cadd = clean.append | ||||
|
r18034 | mexact = match.exact | ||
dirignore = self._dirignore | ||||
checkexec = self._checkexec | ||||
copymap = self._copymap | ||||
lastnormaltime = self._lastnormaltime | ||||
|
r5003 | |||
|
r19191 | # We need to do full walks when either | ||
# - we're listing all clean files, or | ||||
# - match.traversedir does something, because match.traversedir should | ||||
# be called for every dir in the working dir | ||||
full = listclean or match.traversedir is not None | ||||
for fn, st in self.walk(match, subrepos, listunknown, listignored, | ||||
full=full).iteritems(): | ||||
|
r6591 | if fn not in dmap: | ||
|
r18034 | if (listignored or mexact(fn)) and dirignore(fn): | ||
|
r6753 | if listignored: | ||
|
r6033 | iadd(fn) | ||
|
r19910 | else: | ||
|
r5003 | uadd(fn) | ||
|
r1471 | continue | ||
|
r6591 | |||
|
r21810 | # This is equivalent to 'state, mode, size, time = dmap[fn]' but not | ||
# written like that for performance reasons. dmap[fn] is not a | ||||
# Python tuple in compiled builds. The CPython UNPACK_SEQUENCE | ||||
# opcode has fast paths when the value to be unpacked is a tuple or | ||||
# a list, but falls back to creating a full-fledged iterator in | ||||
# general. That is much slower than simply accessing and storing the | ||||
# tuple members one by one. | ||||
t = dmap[fn] | ||||
state = t[0] | ||||
mode = t[1] | ||||
size = t[2] | ||||
time = t[3] | ||||
|
r6591 | |||
|
r6818 | if not st and state in "nma": | ||
dadd(fn) | ||||
elif state == 'n': | ||||
|
r13763 | mtime = int(st.st_mtime) | ||
|
r6257 | if (size >= 0 and | ||
|
r17733 | ((size != st.st_size and size != st.st_size & _rangemask) | ||
|
r25658 | or ((mode ^ st.st_mode) & 0o100 and checkexec)) | ||
|
r10968 | or size == -2 # other parent | ||
|
r18034 | or fn in copymap): | ||
|
r5003 | madd(fn) | ||
|
r19651 | elif time != mtime and time != mtime & _rangemask: | ||
|
r5003 | ladd(fn) | ||
|
r18034 | elif mtime == lastnormaltime: | ||
|
r24181 | # fn may have just been marked as normal and it may have | ||
# changed in the same second without changing its size. | ||||
# This can happen if we quickly do multiple commits. | ||||
|
r13763 | # Force lookup, so we don't miss such a racy file change. | ||
|
r13704 | ladd(fn) | ||
|
r6753 | elif listclean: | ||
|
r5003 | cadd(fn) | ||
|
r6590 | elif state == 'm': | ||
|
r5003 | madd(fn) | ||
|
r6590 | elif state == 'a': | ||
|
r5003 | aadd(fn) | ||
|
r6590 | elif state == 'r': | ||
|
r5003 | radd(fn) | ||
|
r1183 | |||
|
r22913 | return (lookup, scmutil.status(modified, added, removed, deleted, | ||
unknown, ignored, clean)) | ||||
|
r21984 | |||
def matches(self, match): | ||||
''' | ||||
return files in the dirstate (in whatever state) filtered by match | ||||
''' | ||||
dmap = self._map | ||||
if match.always(): | ||||
return dmap.keys() | ||||
files = match.files() | ||||
|
r24448 | if match.isexact(): | ||
|
r21984 | # fast path -- filter the other way around, since typically files is | ||
# much smaller than dmap | ||||
return [f for f in files if f in dmap] | ||||
|
r25275 | if match.prefix() and all(fn in dmap for fn in files): | ||
|
r21984 | # fast path -- all the values are known to be files, so just return | ||
# that | ||||
return list(files) | ||||
return [f for f in dmap if match(f)] | ||||