dirstate.py
1948 lines
| 66.6 KiB
| text/x-python
|
PythonLexer
/ mercurial / dirstate.py
|
r8226 | # dirstate.py - working directory tracking for mercurial | ||
# | ||||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r8226 | # | ||
# 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 | |||
|
r27503 | from __future__ import absolute_import | ||
|
r27670 | import collections | ||
|
r32346 | import contextlib | ||
|
r27503 | import errno | ||
import os | ||||
import stat | ||||
from .i18n import _ | ||||
|
r43360 | from .pycompat import delattr | ||
|
r43533 | |||
from hgdemandimport import tracing | ||||
|
r27503 | from . import ( | ||
encoding, | ||||
error, | ||||
match as matchmod, | ||||
pathutil, | ||||
|
r32372 | policy, | ||
|
r30304 | pycompat, | ||
|
r27503 | scmutil, | ||
|
r45017 | sparse, | ||
|
r31050 | txnutil, | ||
|
r27503 | util, | ||
) | ||||
|
r1089 | |||
|
r43197 | from .interfaces import ( | ||
dirstate as intdirstate, | ||||
util as interfaceutil, | ||||
) | ||||
|
r43906 | parsers = policy.importmod('parsers') | ||
rustmod = policy.importrust('dirstate') | ||||
|
r32372 | |||
|
r8261 | propertycache = util.propertycache | ||
|
r16201 | filecache = scmutil.filecache | ||
|
r43346 | _rangemask = 0x7FFFFFFF | ||
|
r16201 | |||
|
r43000 | dirstatetuple = parsers.dirstatetuple | ||
|
r21808 | |||
|
r43346 | |||
|
r16201 | class repocache(filecache): | ||
"""filecache for files in .hg/""" | ||||
|
r43346 | |||
|
r16201 | def join(self, obj, fname): | ||
return obj._opener.join(fname) | ||||
|
r4610 | |||
|
r43346 | |||
|
r16202 | class rootcache(filecache): | ||
"""filecache for files in the repository root""" | ||||
|
r43346 | |||
|
r16202 | def join(self, obj, fname): | ||
return obj._join(fname) | ||||
|
r43346 | |||
|
r26634 | def _getfsnow(vfs): | ||
'''Get "now" timestamp on filesystem''' | ||||
tmpfd, tmpname = vfs.mkstemp() | ||||
try: | ||||
|
r36799 | return os.fstat(tmpfd)[stat.ST_MTIME] | ||
|
r26634 | finally: | ||
os.close(tmpfd) | ||||
vfs.unlink(tmpname) | ||||
|
r43346 | |||
|
r43197 | @interfaceutil.implementer(intdirstate.idirstate) | ||
|
r1559 | class dirstate(object): | ||
|
r47538 | def __init__( | ||
self, opener, ui, root, validate, sparsematchfn, nodeconstants | ||||
): | ||||
|
r46554 | """Create a new dirstate object. | ||
|
r10145 | |||
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. | ||||
|
r46554 | """ | ||
|
r47538 | self._nodeconstants = nodeconstants | ||
|
r4614 | self._opener = opener | ||
|
r13032 | self._validate = validate | ||
|
r4614 | self._root = root | ||
|
r33373 | self._sparsematchfn = sparsematchfn | ||
|
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 | ||
|
r15791 | self._lastnormaltime = 0 | ||
|
r4614 | self._ui = ui | ||
|
r16200 | self._filecache = {} | ||
|
r22404 | self._parentwriters = 0 | ||
|
r43347 | self._filename = b'dirstate' | ||
self._pendingfilename = b'%s.pending' % self._filename | ||||
|
r29772 | self._plchangecallbacks = {} | ||
self._origpl = None | ||||
|
r31206 | self._updatedfiles = set() | ||
|
r35085 | self._mapcls = dirstatemap | ||
|
r41823 | # Access and cache cwd early, so we don't access it for the first time | ||
# after a working-copy update caused it to not exist (accessing it then | ||||
# raises an exception). | ||||
self._cwd | ||||
|
r22404 | |||
r45359 | def prefetch_parents(self): | |||
"""make sure the parents are loaded | ||||
Used to avoid a race condition. | ||||
""" | ||||
self._pl | ||||
|
r32346 | @contextlib.contextmanager | ||
def parentchange(self): | ||||
|
r46554 | """Context manager for handling dirstate parents. | ||
|
r32346 | |||
If an exception occurs in the scope of the context manager, | ||||
the incoherent dirstate won't be written when wlock is | ||||
released. | ||||
|
r46554 | """ | ||
|
r32346 | self._parentwriters += 1 | ||
yield | ||||
# Typically we want the "undo" step of a context manager in a | ||||
# finally block so it happens even when an exception | ||||
# occurs. In this case, however, we only want to decrement | ||||
# parentwriters if the code in the with statement exits | ||||
# normally, so we don't have a try/finally here on purpose. | ||||
self._parentwriters -= 1 | ||||
|
r22404 | def pendingparentchange(self): | ||
|
r46554 | """Returns true if the dirstate is in the middle of a set of changes | ||
|
r22404 | that modify the dirstate parent. | ||
|
r46554 | """ | ||
|
r22404 | return self._parentwriters > 0 | ||
|
r1089 | |||
|
r8261 | @propertycache | ||
def _map(self): | ||||
|
r35077 | """Return the dirstate contents (see documentation for dirstatemap).""" | ||
|
r47538 | self._map = self._mapcls( | ||
self._ui, self._opener, self._root, self._nodeconstants | ||||
) | ||||
|
r8261 | return self._map | ||
|
r33373 | @property | ||
def _sparsematcher(self): | ||||
"""The matcher for the sparse checkout. | ||||
The working directory may not include every file from a manifest. The | ||||
matcher obtained by this property will match a path if it is to be | ||||
included in the working directory. | ||||
""" | ||||
# TODO there is potential to cache this property. For now, the matcher | ||||
# is resolved on every access. (But the called function does use a | ||||
# cache to keep the lookup fast.) | ||||
return self._sparsematchfn() | ||||
|
r43347 | @repocache(b'branch') | ||
|
r8261 | def _branch(self): | ||
try: | ||||
|
r43347 | return self._opener.read(b"branch").strip() or b"default" | ||
|
r25660 | except IOError as inst: | ||
|
r15799 | if inst.errno != errno.ENOENT: | ||
raise | ||||
|
r43347 | return b"default" | ||
|
r8261 | |||
|
r34340 | @property | ||
|
r8261 | def _pl(self): | ||
|
r34339 | return self._map.parents() | ||
|
r8261 | |||
|
r35083 | def hasdir(self, d): | ||
return self._map.hastrackeddir(d) | ||||
|
r16143 | |||
|
r43347 | @rootcache(b'.hgignore') | ||
|
r8261 | def _ignore(self): | ||
|
r27594 | files = self._ignorefiles() | ||
|
r25216 | if not files: | ||
|
r41825 | return matchmod.never() | ||
|
r25216 | |||
|
r43347 | pats = [b'include:%s' % f for f in files] | ||
return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn) | ||||
|
r8261 | |||
@propertycache | ||||
def _slash(self): | ||||
|
r43347 | return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/' | ||
|
r8261 | |||
@propertycache | ||||
def _checklink(self): | ||||
return util.checklink(self._root) | ||||
@propertycache | ||||
def _checkexec(self): | ||||
|
r45310 | return bool(util.checkexec(self._root)) | ||
|
r8261 | |||
@propertycache | ||||
def _checkcase(self): | ||||
|
r43347 | return not util.fscasesensitive(self._join(b'.hg')) | ||
|
r8261 | |||
|
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: | ||||
|
r43346 | |||
|
r15337 | def f(x): | ||
|
r18869 | try: | ||
st = os.lstat(self._join(x)) | ||||
if util.statislink(st): | ||||
|
r43347 | return b'l' | ||
|
r18869 | if util.statisexec(st): | ||
|
r43347 | return b'x' | ||
|
r18869 | except OSError: | ||
pass | ||||
|
r43347 | return b'' | ||
|
r43346 | |||
|
r15337 | return f | ||
fallback = buildfallback() | ||||
|
r6743 | if self._checklink: | ||
|
r43346 | |||
|
r6743 | def f(x): | ||
|
r6972 | if os.path.islink(self._join(x)): | ||
|
r43347 | return b'l' | ||
if b'x' in fallback(x): | ||||
return b'x' | ||||
return b'' | ||||
|
r43346 | |||
|
r6743 | return f | ||
if self._checkexec: | ||||
|
r43346 | |||
|
r6743 | def f(x): | ||
|
r43347 | if b'l' in fallback(x): | ||
return b'l' | ||||
|
r14273 | if util.isexec(self._join(x)): | ||
|
r43347 | return b'x' | ||
return b'' | ||||
|
r43346 | |||
|
r6743 | return f | ||
|
r15337 | else: | ||
return fallback | ||||
|
r6743 | |||
|
r20335 | @propertycache | ||
def _cwd(self): | ||||
|
r33212 | # internal config: ui.forcecwd | ||
|
r43347 | forcecwd = self._ui.config(b'ui', b'forcecwd') | ||
|
r33212 | if forcecwd: | ||
return forcecwd | ||||
|
r39843 | return encoding.getcwd() | ||
|
r20335 | |||
|
r1089 | def getcwd(self): | ||
|
r46554 | """Return the path from which a canonical path is calculated. | ||
|
r26293 | |||
This path should be used to resolve file patterns or to convert | ||||
canonical paths back to file paths for display. It shouldn't be | ||||
used to get real file paths. Use vfs functions instead. | ||||
|
r46554 | """ | ||
|
r20335 | cwd = self._cwd | ||
|
r10282 | if cwd == self._root: | ||
|
r43347 | return b'' | ||
|
r4614 | # self._root ends with a path separator if self._root is '/' or 'C:\' | ||
rootsep = self._root | ||||
|
r5843 | if not util.endswithsep(rootsep): | ||
|
r30614 | rootsep += pycompat.ossep | ||
|
r4230 | if cwd.startswith(rootsep): | ||
|
r43346 | return cwd[len(rootsep) :] | ||
|
r4230 | 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): | ||
|
r46554 | """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 | ||||
|
r46554 | """ | ||
|
r43347 | return self._map.get(key, (b"?",))[0] | ||
|
r1089 | |||
def __contains__(self, key): | ||||
|
r4614 | return key in self._map | ||
def __iter__(self): | ||||
|
r33673 | return iter(sorted(self._map)) | ||
|
r1089 | |||
|
r32550 | def items(self): | ||
|
r43376 | return pycompat.iteritems(self._map) | ||
|
r18792 | |||
|
r32550 | iteritems = items | ||
|
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 | |||
|
r47758 | def setparents(self, p1, p2=None): | ||
|
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() | ||||
""" | ||||
|
r47758 | if p2 is None: | ||
p2 = self._nodeconstants.nullid | ||||
|
r22407 | if self._parentwriters == 0: | ||
|
r43346 | raise ValueError( | ||
|
r43347 | b"cannot set dirstate parent outside of " | ||
b"dirstate.parentchange context manager" | ||||
|
r43346 | ) | ||
|
r22407 | |||
|
r34340 | self._dirty = True | ||
|
r16509 | oldp2 = self._pl[1] | ||
|
r29772 | if self._origpl is None: | ||
self._origpl = self._pl | ||||
|
r34340 | self._map.setparents(p1, p2) | ||
|
r16551 | copies = {} | ||
|
r47758 | if ( | ||
oldp2 != self._nodeconstants.nullid | ||||
and p2 == self._nodeconstants.nullid | ||||
): | ||||
|
r34675 | candidatefiles = self._map.nonnormalset.union( | ||
|
r43346 | self._map.otherparentset | ||
) | ||||
|
r31278 | for f in candidatefiles: | ||
s = self._map.get(f) | ||||
if s is None: | ||||
continue | ||||
|
r22895 | # Discard 'm' markers when moving away from a merge state | ||
|
r43347 | if s[0] == b'm': | ||
|
r34337 | source = self._map.copymap.get(f) | ||
|
r33983 | if source: | ||
copies[f] = source | ||||
|
r16509 | self.normallookup(f) | ||
|
r22895 | # Also fix up otherparent markers | ||
|
r43347 | elif s[0] == b'n' and s[2] == -2: | ||
|
r34337 | source = self._map.copymap.get(f) | ||
|
r33983 | if source: | ||
copies[f] = source | ||||
|
r22895 | self.add(f) | ||
|
r16551 | return copies | ||
|
r1089 | |||
|
r4179 | def setbranch(self, branch): | ||
|
r40454 | self.__class__._branch.set(self, encoding.fromlocal(branch)) | ||
|
r43347 | f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True) | ||
|
r16472 | try: | ||
|
r43347 | f.write(self._branch + b'\n') | ||
|
r16472 | f.close() | ||
|
r18317 | |||
# make sure filecache has the correct stat info for _branch after | ||||
# replacing the underlying file | ||||
|
r43347 | ce = self._filecache[b'_branch'] | ||
|
r18317 | if ce: | ||
ce.refresh() | ||||
|
r43346 | except: # re-raises | ||
|
r18076 | f.discard() | ||
raise | ||||
|
r4179 | |||
|
r4613 | def invalidate(self): | ||
|
r46554 | """Causes the next access to reread the dirstate. | ||
|
r32682 | |||
This is different from localrepo.invalidatedirstate() because it always | ||||
rereads the dirstate. Use localrepo.invalidatedirstate() if you want to | ||||
|
r46554 | check whether the dirstate has changed before rereading it.""" | ||
|
r32682 | |||
|
r43809 | for a in ("_map", "_branch", "_ignore"): | ||
|
r4953 | if a in self.__dict__: | ||
delattr(self, a) | ||||
|
r15791 | self._lastnormaltime = 0 | ||
|
r4903 | self._dirty = False | ||
|
r31206 | self._updatedfiles.clear() | ||
|
r22404 | self._parentwriters = 0 | ||
|
r29772 | self._origpl = None | ||
|
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: | ||
|
r34337 | self._map.copymap[dest] = source | ||
|
r31206 | self._updatedfiles.add(source) | ||
self._updatedfiles.add(dest) | ||||
|
r34337 | elif self._map.copymap.pop(dest, None): | ||
|
r31206 | self._updatedfiles.add(dest) | ||
|
r1089 | |||
def copied(self, file): | ||||
|
r34337 | return self._map.copymap.get(file, None) | ||
|
r3154 | |||
def copies(self): | ||||
|
r34337 | return self._map.copymap | ||
|
r1089 | |||
|
r17196 | def _addpath(self, f, state, mode, size, mtime): | ||
|
r5487 | oldstate = self[f] | ||
|
r43347 | if state == b'a' or oldstate == b'r': | ||
|
r13974 | scmutil.checkfilename(f) | ||
|
r35083 | if self._map.hastrackeddir(f): | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'directory %r already in dirstate') % pycompat.bytestr(f) | ||
|
r43346 | ) | ||
|
r6767 | # shadows | ||
|
r44032 | for d in pathutil.finddirs(f): | ||
|
r35083 | if self._map.hastrackeddir(d): | ||
|
r6767 | break | ||
|
r34189 | entry = self._map.get(d) | ||
|
r43347 | if entry is not None and entry[0] != b'r': | ||
|
r26587 | raise error.Abort( | ||
|
r43347 | _(b'file %r in dirstate clashes with %r') | ||
|
r43346 | % (pycompat.bytestr(d), pycompat.bytestr(f)) | ||
) | ||||
|
r17094 | self._dirty = True | ||
|
r31206 | self._updatedfiles.add(f) | ||
|
r35080 | self._map.addfile(f, oldstate, state, mode, size, mtime) | ||
|
r5487 | |||
|
r42656 | def normal(self, f, parentfiledata=None): | ||
|
r46554 | """Mark a file normal and clean. | ||
|
r42656 | |||
parentfiledata: (mode, size, mtime) of the clean file | ||||
parentfiledata should be computed from memory (for mode, | ||||
size), as or close as possible from the point where we | ||||
determined the file was clean, to limit the risk of the | ||||
file having been changed by an external process between the | ||||
|
r46554 | moment where the file was determined to be clean and now.""" | ||
|
r42656 | if parentfiledata: | ||
(mode, size, mtime) = parentfiledata | ||||
else: | ||||
s = os.lstat(self._join(f)) | ||||
mode = s.st_mode | ||||
size = s.st_size | ||||
mtime = s[stat.ST_MTIME] | ||||
|
r43347 | self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask) | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
r34675 | if f in self._map.nonnormalset: | ||
self._map.nonnormalset.remove(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.''' | ||
|
r47758 | if self._pl[1] != self._nodeconstants.nullid: | ||
|
r6298 | # 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. | ||||
|
r34189 | entry = self._map.get(f) | ||
if entry is not None: | ||||
|
r43347 | if entry[0] == b'r' and entry[2] in (-1, -2): | ||
|
r34337 | source = self._map.copymap.get(f) | ||
|
r34189 | if entry[2] == -1: | ||
self.merge(f) | ||||
elif entry[2] == -2: | ||||
self.otherparent(f) | ||||
if source: | ||||
self.copy(source, f) | ||||
return | ||||
|
r43347 | if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2: | ||
|
r34189 | return | ||
|
r43347 | self._addpath(f, b'n', 0, -1, -1) | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
r5210 | |||
|
r10968 | def otherparent(self, f): | ||
'''Mark as coming from the other parent, always dirty.''' | ||||
|
r47758 | if self._pl[1] == self._nodeconstants.nullid: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"setting %r to other parent only allowed in merges") % f | ||
|
r43346 | ) | ||
|
r43347 | if f in self and self[f] == b'n': | ||
|
r22896 | # merge-like | ||
|
r43347 | self._addpath(f, b'm', 0, -2, -1) | ||
|
r22896 | else: | ||
# add-like | ||||
|
r43347 | self._addpath(f, b'n', 0, -2, -1) | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
r4904 | |||
def add(self, f): | ||||
|
r10145 | '''Mark a file added.''' | ||
|
r43347 | self._addpath(f, b'a', 0, -1, -1) | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
r4616 | |||
|
r4904 | def remove(self, f): | ||
|
r10145 | '''Mark a file removed.''' | ||
|
r4904 | self._dirty = True | ||
|
r35080 | oldstate = self[f] | ||
|
r6297 | size = 0 | ||
|
r47758 | if self._pl[1] != self._nodeconstants.nullid: | ||
|
r34189 | entry = self._map.get(f) | ||
if entry is not None: | ||||
# backup the previous state | ||||
|
r43347 | if entry[0] == b'm': # merge | ||
|
r34189 | size = -1 | ||
|
r43347 | elif entry[0] == b'n' and entry[2] == -2: # other parent | ||
|
r34189 | size = -2 | ||
|
r34675 | self._map.otherparentset.add(f) | ||
|
r35082 | self._updatedfiles.add(f) | ||
|
r35080 | self._map.removefile(f, oldstate, size) | ||
|
r33983 | if size == 0: | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
r1089 | |||
|
r4904 | def merge(self, f): | ||
|
r10145 | '''Mark a file merged.''' | ||
|
r47758 | if self._pl[1] == self._nodeconstants.nullid: | ||
|
r16509 | return self.normallookup(f) | ||
|
r22897 | return self.otherparent(f) | ||
|
r4904 | |||
|
r14434 | def drop(self, f): | ||
'''Drop a file from the dirstate''' | ||||
|
r35080 | oldstate = self[f] | ||
if self._map.dropfile(f, oldstate): | ||||
|
r15399 | self._dirty = True | ||
|
r35082 | self._updatedfiles.add(f) | ||
|
r34337 | self._map.copymap.pop(f, None) | ||
|
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 | ||||
|
r43347 | if not ignoremissing and b'/' in path: | ||
d, f = path.rsplit(b'/', 1) | ||||
|
r24538 | d = self._normalize(d, False, ignoremissing, None) | ||
|
r43347 | folded = d + b"/" + f | ||
|
r24538 | else: | ||
# No path components, preserve original case | ||||
folded = path | ||||
else: | ||||
# recursively normalize leading directory components | ||||
# against dirstate | ||||
|
r43347 | if b'/' in normed: | ||
d, f = normed.rsplit(b'/', 1) | ||||
|
r24538 | d = self._normalize(d, False, ignoremissing, True) | ||
|
r43347 | r = self._root + b"/" + d | ||
folded = d + b"/" + util.fspath(f, r) | ||||
|
r24538 | 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) | ||
|
r34677 | folded = self._map.filefoldmap.get(normed, None) | ||
|
r13717 | if folded is None: | ||
|
r16542 | if isknown: | ||
|
r13717 | folded = path | ||
|
r7068 | else: | ||
|
r43346 | folded = self._discoverpath( | ||
path, normed, ignoremissing, exists, self._map.filefoldmap | ||||
) | ||||
|
r24539 | return folded | ||
|
r16302 | |||
|
r16542 | def _normalize(self, path, isknown, ignoremissing=False, exists=None): | ||
|
r15488 | normed = util.normcase(path) | ||
|
r34677 | folded = self._map.filefoldmap.get(normed, None) | ||
|
r24561 | if folded is None: | ||
|
r34679 | folded = self._map.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 | ||||
|
r43346 | folded = self._discoverpath( | ||
path, normed, ignoremissing, exists, self._map.dirfoldmap | ||||
) | ||||
|
r13717 | return folded | ||
|
r16542 | def normalize(self, path, isknown=False, ignoremissing=False): | ||
|
r46554 | """ | ||
|
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 | ||||
|
r46554 | """ | ||
|
r13717 | |||
if self._checkcase: | ||||
|
r16542 | return self._normalize(path, isknown, ignoremissing) | ||
|
r13717 | return path | ||
|
r6677 | |||
|
r5065 | def clear(self): | ||
|
r34934 | self._map.clear() | ||
|
r15791 | self._lastnormaltime = 0 | ||
|
r31206 | self._updatedfiles.clear() | ||
|
r5123 | self._dirty = True | ||
|
r5065 | |||
|
r18760 | def rebuild(self, parent, allfiles, changedfiles=None): | ||
|
r25448 | if changedfiles is None: | ||
|
r27176 | # Rebuild entire dirstate | ||
|
r44343 | to_lookup = allfiles | ||
to_drop = [] | ||||
|
r27176 | lastnormaltime = self._lastnormaltime | ||
self.clear() | ||||
self._lastnormaltime = lastnormaltime | ||||
|
r44343 | elif len(changedfiles) < 10: | ||
# Avoid turning allfiles into a set, which can be expensive if it's | ||||
# large. | ||||
to_lookup = [] | ||||
to_drop = [] | ||||
for f in changedfiles: | ||||
if f in allfiles: | ||||
to_lookup.append(f) | ||||
else: | ||||
to_drop.append(f) | ||||
else: | ||||
changedfilesset = set(changedfiles) | ||||
to_lookup = changedfilesset & set(allfiles) | ||||
to_drop = changedfilesset - to_lookup | ||||
|
r27176 | |||
|
r29772 | if self._origpl is None: | ||
self._origpl = self._pl | ||||
|
r47758 | self._map.setparents(parent, self._nodeconstants.nullid) | ||
|
r44343 | |||
for f in to_lookup: | ||||
self.normallookup(f) | ||||
for f in to_drop: | ||||
self.drop(f) | ||||
|
r30026 | |||
|
r4903 | self._dirty = True | ||
|
r1089 | |||
|
r32750 | def identity(self): | ||
|
r46554 | """Return identity of dirstate itself to detect changing in storage | ||
|
r32750 | |||
If identity of previous dirstate is equal to this, writing | ||||
changes based on the former dirstate out can keep consistency. | ||||
|
r46554 | """ | ||
|
r34676 | return self._map.identity | ||
|
r32750 | |||
|
r29673 | def write(self, tr): | ||
|
r4612 | if not self._dirty: | ||
|
r1794 | return | ||
|
r21931 | |||
|
r26634 | filename = self._filename | ||
|
r29673 | if tr: | ||
|
r26634 | # 'dirstate.write()' is not only for writing in-memory | ||
# changes out, but also for dropping ambiguous timestamp. | ||||
# delayed writing re-raise "ambiguous timestamp issue". | ||||
# See also the wiki page below for detail: | ||||
# https://www.mercurial-scm.org/wiki/DirstateTransactionPlan | ||||
# emulate dropping timestamp in 'parsers.pack_dirstate' | ||||
|
r26747 | now = _getfsnow(self._opener) | ||
|
r35079 | self._map.clearambiguoustimes(self._updatedfiles, now) | ||
|
r26634 | |||
# emulate that all 'dirstate.normal' results are written out | ||||
self._lastnormaltime = 0 | ||||
|
r31206 | self._updatedfiles.clear() | ||
|
r26634 | |||
# delay writing in-memory changes out | ||||
|
r43346 | tr.addfilegenerator( | ||
|
r43347 | b'dirstate', | ||
|
r43346 | (self._filename,), | ||
self._writedirstate, | ||||
|
r43347 | location=b'plain', | ||
|
r43346 | ) | ||
|
r26634 | return | ||
|
r43347 | st = self._opener(filename, b"w", atomictemp=True, checkambig=True) | ||
|
r26521 | self._writedirstate(st) | ||
|
r29772 | def addparentchangecallback(self, category, callback): | ||
"""add a callback to be called when the wd parents are changed | ||||
Callback will be called with the following arguments: | ||||
dirstate, (oldp1, oldp2), (newp1, newp2) | ||||
Category is a unique identifier to allow overwriting an old callback | ||||
with a newer callback. | ||||
""" | ||||
self._plchangecallbacks[category] = callback | ||||
|
r26521 | def _writedirstate(self, st): | ||
|
r29772 | # notify callbacks about parents change | ||
if self._origpl is not None and self._origpl != self._pl: | ||||
|
r43376 | for c, callback in sorted( | ||
pycompat.iteritems(self._plchangecallbacks) | ||||
): | ||||
|
r29772 | callback(self, self._origpl, self._pl) | ||
self._origpl = None | ||||
|
r9509 | # use the modification time of the newly created temporary file as the | ||
# filesystem's notion of 'now' | ||||
|
r36799 | now = util.fstat(st)[stat.ST_MTIME] & _rangemask | ||
|
r27397 | |||
# enough 'delaywrite' prevents 'pack_dirstate' from dropping | ||||
# timestamp of each entries in dirstate, because of 'now > mtime' | ||||
|
r43347 | delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite') | ||
|
r27397 | if delaywrite > 0: | ||
|
r27398 | # do we have any files to delay for? | ||
|
r43606 | for f, e in pycompat.iteritems(self._map): | ||
|
r43347 | if e[0] == b'n' and e[3] == now: | ||
|
r43346 | import time # to avoid useless import | ||
|
r27399 | # rather than sleep n seconds, sleep until the next | ||
# multiple of n seconds | ||||
clock = time.time() | ||||
start = int(clock) - (int(clock) % delaywrite) | ||||
end = start + delaywrite | ||||
time.sleep(end - clock) | ||||
|
r43346 | now = end # trust our estimate that the end is near now | ||
|
r27398 | break | ||
|
r27397 | |||
|
r34674 | self._map.write(st, now) | ||
|
r21026 | self._lastnormaltime = 0 | ||
|
r34674 | self._dirty = False | ||
|
r1089 | |||
|
r6032 | def _dirignore(self, f): | ||
if self._ignore(f): | ||||
return True | ||||
|
r44032 | for p in pathutil.finddirs(f): | ||
|
r6767 | if self._ignore(p): | ||
|
r6032 | return True | ||
return False | ||||
|
r27594 | def _ignorefiles(self): | ||
files = [] | ||||
|
r43347 | if os.path.exists(self._join(b'.hgignore')): | ||
files.append(self._join(b'.hgignore')) | ||||
for name, path in self._ui.configitems(b"ui"): | ||||
if name == b'ignore' or name.startswith(b'ignore.'): | ||||
|
r27594 | # 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))) | ||||
return files | ||||
|
r27670 | def _ignorefileandline(self, f): | ||
files = collections.deque(self._ignorefiles()) | ||||
visited = set() | ||||
while files: | ||||
i = files.popleft() | ||||
|
r43346 | patterns = matchmod.readpatternfile( | ||
i, self._ui.warn, sourceinfo=True | ||||
) | ||||
|
r27670 | for pattern, lineno, line in patterns: | ||
|
r43347 | kind, p = matchmod._patsplit(pattern, b'glob') | ||
if kind == b"subinclude": | ||||
|
r27670 | if p not in visited: | ||
files.append(p) | ||||
continue | ||||
|
r43346 | m = matchmod.match( | ||
|
r43347 | self._root, b'', [], [pattern], warn=self._ui.warn | ||
|
r43346 | ) | ||
|
r27670 | if m(f): | ||
return (i, lineno, line) | ||||
visited.add(i) | ||||
|
r43347 | return (None, -1, b"") | ||
|
r27670 | |||
|
r19173 | def _walkexplicit(self, match, subrepos): | ||
|
r46554 | """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 | ||
|
r46554 | directories and that were not found.""" | ||
|
r6578 | |||
|
r8681 | def badtype(mode): | ||
|
r43347 | kind = _(b'unknown') | ||
|
r10282 | if stat.S_ISCHR(mode): | ||
|
r43347 | kind = _(b'character device') | ||
|
r10282 | elif stat.S_ISBLK(mode): | ||
|
r43347 | kind = _(b'block device') | ||
|
r10282 | elif stat.S_ISFIFO(mode): | ||
|
r43347 | kind = _(b'fifo') | ||
|
r10282 | elif stat.S_ISSOCK(mode): | ||
|
r43347 | kind = _(b'socket') | ||
|
r10282 | elif stat.S_ISDIR(mode): | ||
|
r43347 | kind = _(b'directory') | ||
return _(b'unsupported file type (type is %s)') % kind | ||||
|
r6830 | |||
|
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): | ||||
|
r43347 | subpath = subrepos[j] + b"/" | ||
|
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 | ||||
|
r43347 | if not files or b'' in files: | ||
files = [b''] | ||||
|
r42527 | # constructing the foldmap is expensive, so don't do it for the | ||
|
r42528 | # common case where files is [''] | ||
|
r42527 | normalize = None | ||
|
r10176 | results = dict.fromkeys(subrepos) | ||
|
r43347 | results[b'.hg'] = None | ||
|
r5000 | |||
|
r12211 | for ff in files: | ||
|
r42527 | if normalize: | ||
|
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 | ||
|
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 | ||
|
r43346 | except OSError as inst: # nf not found on disk - it is dirstate only | ||
if nf in dmap: # does it exactly match a missing file? | ||||
|
r8675 | results[nf] = None | ||
|
r43346 | else: # does it match a missing directory? | ||
|
r35083 | if self._map.hasdir(nf): | ||
|
r23375 | notfoundadd(nf) | ||
|
r8677 | else: | ||
|
r34024 | badfn(ff, encoding.strtolocal(inst.strerror)) | ||
|
r6820 | |||
|
r36218 | # match.files() may contain explicitly-specified paths that shouldn't | ||
# be taken; drop them from the list of files found. dirsfound/notfound | ||||
# aren't filtered here because they will be tested later. | ||||
if match.anypats(): | ||||
for f in list(results): | ||||
|
r43347 | if f == b'.hg' or f in subrepos: | ||
|
r36218 | # keep sentinel to disable further out-of-repo walks | ||
continue | ||||
if not match(f): | ||||
del results[f] | ||||
|
r25877 | # Case insensitive filesystems cannot rely on lstat() failing to detect | ||
# a case-only rename. Prune the stat object for any file that does not | ||||
# match the case in the filesystem, if there are multiple files that | ||||
# normalize to the same path. | ||||
if match.isexact() and self._checkcase: | ||||
normed = {} | ||||
|
r43376 | for f, st in pycompat.iteritems(results): | ||
|
r25877 | if st is None: | ||
continue | ||||
nc = util.normcase(f) | ||||
paths = normed.get(nc) | ||||
if paths is None: | ||||
paths = set() | ||||
normed[nc] = paths | ||||
paths.add(f) | ||||
|
r43376 | for norm, paths in pycompat.iteritems(normed): | ||
|
r25877 | if len(paths) > 1: | ||
for path in paths: | ||||
|
r43346 | folded = self._discoverpath( | ||
path, norm, True, None, self._map.dirfoldmap | ||||
) | ||||
|
r25877 | if path != folded: | ||
results[path] = None | ||||
|
r19174 | return results, dirsfound, dirsnotfound | ||
|
r19173 | |||
|
r19190 | def walk(self, match, subrepos, unknown, ignored, full=True): | ||
|
r46554 | """ | ||
|
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 | |||
|
r46554 | """ | ||
|
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 | ||||
|
r32203 | listdir = util.listdir | ||
|
r19173 | lstat = os.lstat | ||
dirkind = stat.S_IFDIR | ||||
regkind = stat.S_IFREG | ||||
lnkkind = stat.S_IFLNK | ||||
join = self._join | ||||
exact = skipstep3 = False | ||||
|
r43346 | if match.isexact(): # match.exact | ||
|
r19173 | exact = True | ||
|
r43346 | dirignore = util.always # skip step 2 | ||
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) | ||||
|
r44112 | if matchtdir: | ||
for d in work: | ||||
matchtdir(d[0]) | ||||
for d in dirsnotfound: | ||||
matchtdir(d) | ||||
|
r19173 | |||
|
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: | ||||
|
r43533 | tracing.counter('dirstate.walk work', len(work)) | ||
|
r24560 | nd = work.pop() | ||
|
r38991 | visitentries = match.visitchildrenset(nd) | ||
if not visitentries: | ||||
|
r32177 | continue | ||
|
r43347 | if visitentries == b'this' or visitentries == b'all': | ||
|
r38991 | visitentries = None | ||
|
r24559 | skip = None | ||
|
r43347 | if nd != b'': | ||
skip = b'.hg' | ||||
|
r24559 | try: | ||
|
r43533 | with tracing.log('dirstate.walk.traverse listdir %s', nd): | ||
entries = listdir(join(nd), stat=True, skip=skip) | ||||
|
r25660 | except OSError as inst: | ||
|
r24559 | if inst.errno in (errno.EACCES, errno.ENOENT): | ||
|
r43346 | match.bad( | ||
self.pathto(nd), encoding.strtolocal(inst.strerror) | ||||
) | ||||
|
r24559 | continue | ||
raise | ||||
for f, kind, st in entries: | ||||
|
r39296 | # Some matchers may return files in the visitentries set, | ||
# instead of 'this', if the matcher explicitly mentions them | ||||
# and is not an exactmatcher. This is acceptable; we do not | ||||
# make any hard assumptions about file-or-directory below | ||||
# based on the presence of `f` in visitentries. If | ||||
# visitchildrenset returned a set, we can always skip the | ||||
# entries *not* in the set it provided regardless of whether | ||||
# they're actually a file or a directory. | ||||
|
r38991 | if visitentries and f not in visitentries: | ||
continue | ||||
|
r24559 | 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 | ||||
|
r43346 | nf = normalizefile( | ||
|
r43347 | nd and (nd + b"/" + f) or f, True, True | ||
|
r43346 | ) | ||
|
r24559 | else: | ||
|
r43347 | nf = nd and (nd + b"/" + f) or f | ||
|
r24559 | 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 | ||||
|
r43346 | elif (matchalways or matchfn(nf)) and not ignore( | ||
nf | ||||
): | ||||
|
r24560 | # 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] | ||||
|
r43347 | del results[b'.hg'] | ||
|
r18812 | |||
|
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: | ||
|
r31422 | visit = [f for f in dmap] | ||
|
r18815 | 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. | ||||
|
r33722 | audit_path = pathutil.pathauditor(self._root, cached=True) | ||
|
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. | ||||
|
r43346 | 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. | ||
|
r31507 | iv = iter(visit) | ||
|
r26984 | for st in util.statfiles([join(i) for i in visit]): | ||
|
r31507 | results[next(iv)] = st | ||
|
r6829 | return results | ||
|
r1089 | |||
|
r45017 | def _rust_status(self, matcher, list_clean, list_ignored, list_unknown): | ||
|
r44537 | # Force Rayon (Rust parallelism library) to respect the number of | ||
# workers. This is a temporary workaround until Rust code knows | ||||
# how to read the config file. | ||||
numcpus = self._ui.configint(b"worker", b"numcpus") | ||||
if numcpus is not None: | ||||
encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus) | ||||
workers_enabled = self._ui.configbool(b"worker", b"enabled", True) | ||||
if not workers_enabled: | ||||
encoding.environ[b"RAYON_NUM_THREADS"] = b"1" | ||||
( | ||||
lookup, | ||||
modified, | ||||
added, | ||||
removed, | ||||
deleted, | ||||
|
r45017 | clean, | ||
ignored, | ||||
|
r44537 | unknown, | ||
|
r45017 | warnings, | ||
bad, | ||||
|
r45355 | traversed, | ||
|
r44537 | ) = rustmod.status( | ||
self._map._rustmap, | ||||
matcher, | ||||
self._rootdir, | ||||
|
r45017 | self._ignorefiles(), | ||
self._checkexec, | ||||
|
r44537 | self._lastnormaltime, | ||
|
r45017 | bool(list_clean), | ||
bool(list_ignored), | ||||
bool(list_unknown), | ||||
|
r45355 | bool(matcher.traversedir), | ||
|
r44537 | ) | ||
|
r45355 | |||
if matcher.traversedir: | ||||
for dir in traversed: | ||||
matcher.traversedir(dir) | ||||
|
r45017 | if self._ui.warn: | ||
for item in warnings: | ||||
if isinstance(item, tuple): | ||||
file_path, syntax = item | ||||
msg = _(b"%s: ignoring invalid syntax '%s'\n") % ( | ||||
file_path, | ||||
syntax, | ||||
) | ||||
self._ui.warn(msg) | ||||
else: | ||||
msg = _(b"skipping unreadable pattern file '%s': %s\n") | ||||
self._ui.warn( | ||||
msg | ||||
% ( | ||||
pathutil.canonpath( | ||||
self._rootdir, self._rootdir, item | ||||
), | ||||
b"No such file or directory", | ||||
) | ||||
) | ||||
for (fn, message) in bad: | ||||
matcher.bad(fn, encoding.strtolocal(message)) | ||||
|
r44537 | |||
status = scmutil.status( | ||||
modified=modified, | ||||
added=added, | ||||
removed=removed, | ||||
deleted=deleted, | ||||
unknown=unknown, | ||||
|
r45017 | ignored=ignored, | ||
|
r44537 | clean=clean, | ||
) | ||||
return (lookup, status) | ||||
|
r10176 | def status(self, match, subrepos, ignored, clean, unknown): | ||
|
r46554 | """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 | ||||
|
r46554 | """ | ||
|
r6753 | listignored, listclean, listunknown = ignored, clean, unknown | ||
|
r2022 | lookup, modified, added, unknown, ignored = [], [], [], [], [] | ||
|
r2661 | removed, deleted, clean = [], [], [] | ||
|
r1089 | |||
|
r5003 | dmap = self._map | ||
|
r34936 | dmap.preload() | ||
|
r43568 | |||
use_rust = True | ||||
|
r44369 | |||
|
r45017 | allowed_matchers = ( | ||
matchmod.alwaysmatcher, | ||||
matchmod.exactmatcher, | ||||
matchmod.includematcher, | ||||
) | ||||
|
r44369 | |||
|
r43568 | if rustmod is None: | ||
use_rust = False | ||||
|
r45017 | elif self._checkcase: | ||
# Case-insensitive filesystems are not handled yet | ||||
use_rust = False | ||||
|
r43568 | elif subrepos: | ||
use_rust = False | ||||
|
r45017 | elif sparse.enabled: | ||
|
r43568 | use_rust = False | ||
|
r44369 | elif not isinstance(match, allowed_matchers): | ||
|
r45351 | # Some matchers have yet to be implemented | ||
|
r43568 | use_rust = False | ||
if use_rust: | ||||
|
r45017 | try: | ||
return self._rust_status( | ||||
match, listclean, listignored, listunknown | ||||
) | ||||
except rustmod.FallbackError: | ||||
pass | ||||
|
r43568 | |||
|
r44019 | def noop(f): | ||
pass | ||||
|
r34936 | dcontains = dmap.__contains__ | ||
dget = dmap.__getitem__ | ||||
|
r43346 | ladd = lookup.append # aka "unsure" | ||
|
r5003 | madd = modified.append | ||
aadd = added.append | ||||
|
r44019 | uadd = unknown.append if listunknown else noop | ||
iadd = ignored.append if listignored else noop | ||||
|
r5003 | radd = removed.append | ||
dadd = deleted.append | ||||
|
r44019 | cadd = clean.append if listclean else noop | ||
|
r18034 | mexact = match.exact | ||
dirignore = self._dirignore | ||||
checkexec = self._checkexec | ||||
|
r34337 | copymap = self._map.copymap | ||
|
r18034 | 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 | ||||
|
r43376 | for fn, st in pycompat.iteritems( | ||
self.walk(match, subrepos, listunknown, listignored, full=full) | ||||
): | ||||
|
r34936 | if not dcontains(fn): | ||
|
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. | ||||
|
r34936 | t = dget(fn) | ||
|
r21810 | state = t[0] | ||
mode = t[1] | ||||
size = t[2] | ||||
time = t[3] | ||||
|
r6591 | |||
|
r43347 | if not st and state in b"nma": | ||
|
r6818 | dadd(fn) | ||
|
r43347 | elif state == b'n': | ||
|
r43346 | if ( | ||
size >= 0 | ||||
and ( | ||||
(size != st.st_size and size != st.st_size & _rangemask) | ||||
or ((mode ^ st.st_mode) & 0o100 and checkexec) | ||||
) | ||||
or size == -2 # other parent | ||||
or fn in copymap | ||||
): | ||||
|
r47530 | if stat.S_ISLNK(st.st_mode) and size != st.st_size: | ||
|
r47436 | # issue6456: Size returned may be longer due to | ||
# encryption on EXT-4 fscrypt, undecided. | ||||
ladd(fn) | ||||
else: | ||||
madd(fn) | ||||
|
r43346 | elif ( | ||
time != st[stat.ST_MTIME] | ||||
and time != st[stat.ST_MTIME] & _rangemask | ||||
): | ||||
|
r5003 | ladd(fn) | ||
|
r36799 | elif st[stat.ST_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) | ||
|
r43347 | elif state == b'm': | ||
|
r5003 | madd(fn) | ||
|
r43347 | elif state == b'a': | ||
|
r5003 | aadd(fn) | ||
|
r43347 | elif state == b'r': | ||
|
r5003 | radd(fn) | ||
|
r45017 | status = scmutil.status( | ||
modified, added, removed, deleted, unknown, ignored, clean | ||||
|
r43346 | ) | ||
|
r45017 | return (lookup, status) | ||
|
r21984 | |||
def matches(self, match): | ||||
|
r46554 | """ | ||
|
r21984 | return files in the dirstate (in whatever state) filtered by match | ||
|
r46554 | """ | ||
|
r21984 | dmap = self._map | ||
|
r44833 | if rustmod is not None: | ||
dmap = self._map._rustmap | ||||
|
r21984 | 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)] | ||||
|
r26632 | |||
|
r26746 | def _actualfilename(self, tr): | ||
if tr: | ||||
|
r26633 | return self._pendingfilename | ||
else: | ||||
return self._filename | ||||
|
r33440 | def savebackup(self, tr, backupname): | ||
'''Save current dirstate into backup file''' | ||||
|
r26746 | filename = self._actualfilename(tr) | ||
|
r33440 | assert backupname != filename | ||
|
r26633 | |||
# use '_writedirstate' instead of 'write' to write changes certainly, | ||||
# because the latter omits writing out if transaction is running. | ||||
# output file will be used to create backup of dirstate at this point. | ||||
|
r31208 | if self._dirty or not self._opener.exists(filename): | ||
|
r43346 | self._writedirstate( | ||
|
r43347 | self._opener(filename, b"w", atomictemp=True, checkambig=True) | ||
|
r43346 | ) | ||
|
r26633 | |||
if tr: | ||||
# ensure that subsequent tr.writepending returns True for | ||||
# changes written out above, even if dirstate is never | ||||
# changed after this | ||||
|
r43346 | tr.addfilegenerator( | ||
|
r43347 | b'dirstate', | ||
|
r43346 | (self._filename,), | ||
self._writedirstate, | ||||
|
r43347 | location=b'plain', | ||
|
r43346 | ) | ||
|
r26633 | |||
# ensure that pending file written above is unlinked at | ||||
# failure, even if tr.writepending isn't invoked until the | ||||
# end of this transaction | ||||
|
r43347 | tr.registertmp(filename, location=b'plain') | ||
|
r26633 | |||
|
r31547 | self._opener.tryunlink(backupname) | ||
|
r31207 | # hardlink backup is okay because _writedirstate is always called | ||
# with an "atomictemp=True" file. | ||||
|
r43346 | util.copyfile( | ||
self._opener.join(filename), | ||||
self._opener.join(backupname), | ||||
hardlink=True, | ||||
) | ||||
|
r26632 | |||
|
r33440 | def restorebackup(self, tr, backupname): | ||
'''Restore dirstate by backup file''' | ||||
|
r26632 | # this "invalidate()" prevents "wlock.release()" from writing | ||
# changes of dirstate out after restoring from backup file | ||||
self.invalidate() | ||||
|
r26746 | filename = self._actualfilename(tr) | ||
|
r34941 | o = self._opener | ||
if util.samefile(o.join(backupname), o.join(filename)): | ||||
o.unlink(backupname) | ||||
else: | ||||
o.rename(backupname, filename, checkambig=True) | ||||
|
r26632 | |||
|
r33440 | def clearbackup(self, tr, backupname): | ||
'''Clear backup file''' | ||||
self._opener.unlink(backupname) | ||||
|
r34333 | |||
|
r43346 | |||
|
r34333 | class dirstatemap(object): | ||
|
r35077 | """Map encapsulating the dirstate's contents. | ||
The dirstate contains the following state: | ||||
- `identity` is the identity of the dirstate file, which can be used to | ||||
detect when changes have occurred to the dirstate file. | ||||
- `parents` is a pair containing the parents of the working copy. The | ||||
parents are updated by calling `setparents`. | ||||
- the state map maps filenames to tuples of (state, mode, size, mtime), | ||||
where state is a single character representing 'normal', 'added', | ||||
|
r35078 | 'removed', or 'merged'. It is read by treating the dirstate as a | ||
dict. File state is updated by calling the `addfile`, `removefile` and | ||||
`dropfile` methods. | ||||
|
r35077 | |||
- `copymap` maps destination filenames to their source filename. | ||||
The dirstate also provides the following views onto the state: | ||||
- `nonnormalset` is a set of the filenames that have state other | ||||
than 'normal', or are normal but have an mtime of -1 ('normallookup'). | ||||
- `otherparentset` is a set of the filenames that are marked as coming | ||||
from the second parent when the dirstate is currently being merged. | ||||
- `filefoldmap` is a dict mapping normalized filenames to the denormalized | ||||
form that they appear as in the dirstate. | ||||
- `dirfoldmap` is a dict mapping normalized directory names to the | ||||
denormalized form that they appear as in the dirstate. | ||||
""" | ||||
|
r47538 | def __init__(self, ui, opener, root, nodeconstants): | ||
|
r34338 | self._ui = ui | ||
self._opener = opener | ||||
self._root = root | ||||
|
r43347 | self._filename = b'dirstate' | ||
|
r45778 | self._nodelen = 20 | ||
|
r47538 | self._nodeconstants = nodeconstants | ||
|
r34338 | |||
|
r34340 | self._parents = None | ||
self._dirtyparents = False | ||||
|
r34333 | |||
|
r34338 | # for consistent view between _pl() and _read() invocations | ||
self._pendingmode = None | ||||
|
r34935 | @propertycache | ||
def _map(self): | ||||
self._map = {} | ||||
self.read() | ||||
return self._map | ||||
@propertycache | ||||
def copymap(self): | ||||
self.copymap = {} | ||||
self._map | ||||
return self.copymap | ||||
|
r34934 | def clear(self): | ||
|
r34936 | self._map.clear() | ||
self.copymap.clear() | ||||
|
r47758 | self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid) | ||
|
r43347 | util.clearcachedproperty(self, b"_dirs") | ||
util.clearcachedproperty(self, b"_alldirs") | ||||
util.clearcachedproperty(self, b"filefoldmap") | ||||
util.clearcachedproperty(self, b"dirfoldmap") | ||||
util.clearcachedproperty(self, b"nonnormalset") | ||||
util.clearcachedproperty(self, b"otherparentset") | ||||
|
r34934 | |||
|
r35896 | def items(self): | ||
|
r43376 | return pycompat.iteritems(self._map) | ||
|
r34333 | |||
|
r35896 | # forward for python2,3 compat | ||
iteritems = items | ||||
|
r34409 | def __len__(self): | ||
return len(self._map) | ||||
|
r34333 | def __iter__(self): | ||
return iter(self._map) | ||||
def get(self, key, default=None): | ||||
return self._map.get(key, default) | ||||
def __contains__(self, key): | ||||
return key in self._map | ||||
def __getitem__(self, key): | ||||
return self._map[key] | ||||
def keys(self): | ||||
return self._map.keys() | ||||
|
r34334 | |||
|
r34936 | def preload(self): | ||
"""Loads the underlying data, if it's not already loaded""" | ||||
self._map | ||||
|
r35080 | def addfile(self, f, oldstate, state, mode, size, mtime): | ||
|
r35078 | """Add a tracked file to the dirstate.""" | ||
|
r43809 | if oldstate in b"?r" and "_dirs" in self.__dict__: | ||
|
r35083 | self._dirs.addpath(f) | ||
|
r43809 | if oldstate == b"?" and "_alldirs" in self.__dict__: | ||
|
r35083 | self._alldirs.addpath(f) | ||
|
r35078 | self._map[f] = dirstatetuple(state, mode, size, mtime) | ||
|
r43347 | if state != b'n' or mtime == -1: | ||
|
r35079 | self.nonnormalset.add(f) | ||
if size == -2: | ||||
self.otherparentset.add(f) | ||||
|
r35078 | |||
|
r35080 | def removefile(self, f, oldstate, size): | ||
|
r35078 | """ | ||
Mark a file as removed in the dirstate. | ||||
The `size` parameter is used to store sentinel values that indicate | ||||
the file's previous state. In the future, we should refactor this | ||||
to be more explicit about what that state is. | ||||
""" | ||||
|
r43809 | if oldstate not in b"?r" and "_dirs" in self.__dict__: | ||
|
r35083 | self._dirs.delpath(f) | ||
|
r43809 | if oldstate == b"?" and "_alldirs" in self.__dict__: | ||
|
r35083 | self._alldirs.addpath(f) | ||
|
r43809 | if "filefoldmap" in self.__dict__: | ||
|
r35081 | normed = util.normcase(f) | ||
self.filefoldmap.pop(normed, None) | ||||
|
r43347 | self._map[f] = dirstatetuple(b'r', 0, size, 0) | ||
|
r35079 | self.nonnormalset.add(f) | ||
|
r35078 | |||
|
r35080 | def dropfile(self, f, oldstate): | ||
|
r35078 | """ | ||
Remove a file from the dirstate. Returns True if the file was | ||||
previously recorded. | ||||
""" | ||||
|
r35079 | exists = self._map.pop(f, None) is not None | ||
|
r35080 | if exists: | ||
|
r43809 | if oldstate != b"r" and "_dirs" in self.__dict__: | ||
|
r35083 | self._dirs.delpath(f) | ||
|
r43809 | if "_alldirs" in self.__dict__: | ||
|
r35083 | self._alldirs.delpath(f) | ||
|
r43809 | if "filefoldmap" in self.__dict__: | ||
|
r35081 | normed = util.normcase(f) | ||
self.filefoldmap.pop(normed, None) | ||||
|
r35079 | self.nonnormalset.discard(f) | ||
return exists | ||||
def clearambiguoustimes(self, files, now): | ||||
for f in files: | ||||
e = self.get(f) | ||||
|
r43347 | if e is not None and e[0] == b'n' and e[3] == now: | ||
|
r35079 | self._map[f] = dirstatetuple(e[0], e[1], e[2], -1) | ||
self.nonnormalset.add(f) | ||||
|
r35078 | |||
|
r34334 | def nonnormalentries(self): | ||
'''Compute the nonnormal dirstate entries from the dmap''' | ||||
try: | ||||
return parsers.nonnormalotherparententries(self._map) | ||||
except AttributeError: | ||||
nonnorm = set() | ||||
otherparent = set() | ||||
|
r43376 | for fname, e in pycompat.iteritems(self._map): | ||
|
r43347 | if e[0] != b'n' or e[3] == -1: | ||
|
r34334 | nonnorm.add(fname) | ||
|
r43347 | if e[0] == b'n' and e[2] == -2: | ||
|
r34334 | otherparent.add(fname) | ||
return nonnorm, otherparent | ||||
|
r34677 | @propertycache | ||
|
r34335 | def filefoldmap(self): | ||
"""Returns a dictionary mapping normalized case paths to their | ||||
non-normalized versions. | ||||
""" | ||||
try: | ||||
makefilefoldmap = parsers.make_file_foldmap | ||||
except AttributeError: | ||||
pass | ||||
else: | ||||
|
r43346 | return makefilefoldmap( | ||
self._map, util.normcasespec, util.normcasefallback | ||||
) | ||||
|
r34335 | |||
f = {} | ||||
normcase = util.normcase | ||||
|
r43376 | for name, s in pycompat.iteritems(self._map): | ||
|
r43347 | if s[0] != b'r': | ||
|
r34335 | f[normcase(name)] = name | ||
|
r43347 | f[b'.'] = b'.' # prevents useless util.fspath() invocation | ||
|
r34335 | return f | ||
|
r34336 | |||
|
r35083 | def hastrackeddir(self, d): | ||
""" | ||||
Returns True if the dirstate contains a tracked (not removed) file | ||||
in this directory. | ||||
""" | ||||
return d in self._dirs | ||||
def hasdir(self, d): | ||||
""" | ||||
Returns True if the dirstate contains a file (tracked or removed) | ||||
in this directory. | ||||
""" | ||||
return d in self._alldirs | ||||
|
r34678 | @propertycache | ||
|
r35083 | def _dirs(self): | ||
r43923 | return pathutil.dirs(self._map, b'r') | |||
|
r34338 | |||
|
r35083 | @propertycache | ||
def _alldirs(self): | ||||
r43923 | return pathutil.dirs(self._map) | |||
|
r35083 | |||
|
r34338 | def _opendirstatefile(self): | ||
fp, mode = txnutil.trypending(self._root, self._opener, self._filename) | ||||
if self._pendingmode is not None and self._pendingmode != mode: | ||||
fp.close() | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'working directory state may be changed parallelly') | ||
|
r43346 | ) | ||
|
r34338 | self._pendingmode = mode | ||
return fp | ||||
|
r34339 | def parents(self): | ||
|
r34340 | if not self._parents: | ||
try: | ||||
fp = self._opendirstatefile() | ||||
|
r45778 | st = fp.read(2 * self._nodelen) | ||
|
r34340 | fp.close() | ||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
# File doesn't exist, so the current state is empty | ||||
|
r43347 | st = b'' | ||
|
r34340 | |||
|
r34339 | l = len(st) | ||
|
r45778 | if l == self._nodelen * 2: | ||
self._parents = ( | ||||
st[: self._nodelen], | ||||
st[self._nodelen : 2 * self._nodelen], | ||||
) | ||||
|
r34340 | elif l == 0: | ||
|
r47758 | self._parents = ( | ||
self._nodeconstants.nullid, | ||||
self._nodeconstants.nullid, | ||||
) | ||||
|
r34340 | else: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'working directory state appears damaged!') | ||
|
r43346 | ) | ||
|
r34340 | |||
return self._parents | ||||
def setparents(self, p1, p2): | ||||
self._parents = (p1, p2) | ||||
self._dirtyparents = True | ||||
|
r34673 | |||
def read(self): | ||||
|
r34676 | # ignore HG_PENDING because identity is used only for writing | ||
self.identity = util.filestat.frompath( | ||||
|
r43346 | self._opener.join(self._filename) | ||
) | ||||
|
r34676 | |||
|
r34673 | try: | ||
fp = self._opendirstatefile() | ||||
try: | ||||
st = fp.read() | ||||
finally: | ||||
fp.close() | ||||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
return | ||||
if not st: | ||||
return | ||||
|
r43347 | if util.safehasattr(parsers, b'dict_new_presized'): | ||
|
r34673 | # Make an estimate of the number of files in the dirstate based on | ||
|
r45803 | # its size. This trades wasting some memory for avoiding costly | ||
# resizes. Each entry have a prefix of 17 bytes followed by one or | ||||
# two path names. Studies on various large-scale real-world repositories | ||||
# found 54 bytes a reasonable upper limit for the average path names. | ||||
# Copy entries are ignored for the sake of this estimate. | ||||
self._map = parsers.dict_new_presized(len(st) // 71) | ||||
|
r34673 | |||
# 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. | ||||
# | ||||
# (we cannot decorate the function directly since it is in a C module) | ||||
|
r42992 | parse_dirstate = util.nogc(parsers.parse_dirstate) | ||
|
r34673 | p = parse_dirstate(self._map, self.copymap, st) | ||
if not self._dirtyparents: | ||||
self.setparents(*p) | ||||
|
r34674 | |||
|
r34936 | # Avoid excess attribute lookups by fast pathing certain checks | ||
self.__contains__ = self._map.__contains__ | ||||
self.__getitem__ = self._map.__getitem__ | ||||
self.get = self._map.get | ||||
|
r34674 | def write(self, st, now): | ||
|
r43346 | st.write( | ||
parsers.pack_dirstate(self._map, self.copymap, self.parents(), now) | ||||
) | ||||
|
r34674 | st.close() | ||
self._dirtyparents = False | ||||
|
r34675 | self.nonnormalset, self.otherparentset = self.nonnormalentries() | ||
@propertycache | ||||
def nonnormalset(self): | ||||
nonnorm, otherparents = self.nonnormalentries() | ||||
self.otherparentset = otherparents | ||||
return nonnorm | ||||
@propertycache | ||||
def otherparentset(self): | ||||
nonnorm, otherparents = self.nonnormalentries() | ||||
self.nonnormalset = nonnorm | ||||
return otherparents | ||||
|
r34676 | @propertycache | ||
def identity(self): | ||||
|
r34935 | self._map | ||
|
r34676 | return self.identity | ||
|
r34679 | @propertycache | ||
def dirfoldmap(self): | ||||
f = {} | ||||
normcase = util.normcase | ||||
|
r35083 | for name in self._dirs: | ||
|
r34679 | f[normcase(name)] = name | ||
return f | ||||
|
r43000 | |||
if rustmod is not None: | ||||
|
r43346 | |||
|
r43000 | class dirstatemap(object): | ||
|
r47538 | def __init__(self, ui, opener, root, nodeconstants): | ||
self._nodeconstants = nodeconstants | ||||
|
r43000 | self._ui = ui | ||
self._opener = opener | ||||
self._root = root | ||||
|
r43347 | self._filename = b'dirstate' | ||
|
r43000 | self._parents = None | ||
self._dirtyparents = False | ||||
# for consistent view between _pl() and _read() invocations | ||||
self._pendingmode = None | ||||
def addfile(self, *args, **kwargs): | ||||
return self._rustmap.addfile(*args, **kwargs) | ||||
def removefile(self, *args, **kwargs): | ||||
return self._rustmap.removefile(*args, **kwargs) | ||||
def dropfile(self, *args, **kwargs): | ||||
return self._rustmap.dropfile(*args, **kwargs) | ||||
def clearambiguoustimes(self, *args, **kwargs): | ||||
return self._rustmap.clearambiguoustimes(*args, **kwargs) | ||||
def nonnormalentries(self): | ||||
return self._rustmap.nonnormalentries() | ||||
def get(self, *args, **kwargs): | ||||
return self._rustmap.get(*args, **kwargs) | ||||
@propertycache | ||||
def _rustmap(self): | ||||
|
r45358 | """ | ||
Fills the Dirstatemap when called. | ||||
Use `self._inner_rustmap` if reading the dirstate is not necessary. | ||||
""" | ||||
self._rustmap = self._inner_rustmap | ||||
|
r43000 | self.read() | ||
return self._rustmap | ||||
|
r45358 | @propertycache | ||
def _inner_rustmap(self): | ||||
""" | ||||
Does not fill the Dirstatemap when called. This allows for | ||||
optimizations where only setting/getting the parents is needed. | ||||
""" | ||||
self._inner_rustmap = rustmod.DirstateMap(self._root) | ||||
return self._inner_rustmap | ||||
|
r43000 | @property | ||
def copymap(self): | ||||
return self._rustmap.copymap() | ||||
def preload(self): | ||||
self._rustmap | ||||
def clear(self): | ||||
self._rustmap.clear() | ||||
|
r45358 | self._inner_rustmap.clear() | ||
|
r47758 | self.setparents( | ||
self._nodeconstants.nullid, self._nodeconstants.nullid | ||||
) | ||||
|
r43347 | util.clearcachedproperty(self, b"_dirs") | ||
util.clearcachedproperty(self, b"_alldirs") | ||||
util.clearcachedproperty(self, b"dirfoldmap") | ||||
|
r43000 | |||
def items(self): | ||||
return self._rustmap.items() | ||||
def keys(self): | ||||
return iter(self._rustmap) | ||||
def __contains__(self, key): | ||||
return key in self._rustmap | ||||
def __getitem__(self, item): | ||||
return self._rustmap[item] | ||||
def __len__(self): | ||||
return len(self._rustmap) | ||||
def __iter__(self): | ||||
return iter(self._rustmap) | ||||
# forward for python2,3 compat | ||||
iteritems = items | ||||
def _opendirstatefile(self): | ||||
|
r43346 | fp, mode = txnutil.trypending( | ||
self._root, self._opener, self._filename | ||||
) | ||||
|
r43000 | if self._pendingmode is not None and self._pendingmode != mode: | ||
fp.close() | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'working directory state may be changed parallelly') | ||
|
r43346 | ) | ||
|
r43000 | self._pendingmode = mode | ||
return fp | ||||
def setparents(self, p1, p2): | ||||
self._rustmap.setparents(p1, p2) | ||||
self._parents = (p1, p2) | ||||
self._dirtyparents = True | ||||
def parents(self): | ||||
if not self._parents: | ||||
try: | ||||
fp = self._opendirstatefile() | ||||
|
r45801 | st = fp.read(40) | ||
|
r43000 | fp.close() | ||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
# File doesn't exist, so the current state is empty | ||||
|
r43347 | st = b'' | ||
|
r43000 | |||
try: | ||||
|
r45358 | self._parents = self._inner_rustmap.parents(st) | ||
|
r43000 | except ValueError: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'working directory state appears damaged!') | ||
|
r43346 | ) | ||
|
r43000 | |||
return self._parents | ||||
def read(self): | ||||
# ignore HG_PENDING because identity is used only for writing | ||||
self.identity = util.filestat.frompath( | ||||
|
r43346 | self._opener.join(self._filename) | ||
) | ||||
|
r43000 | |||
try: | ||||
fp = self._opendirstatefile() | ||||
try: | ||||
st = fp.read() | ||||
finally: | ||||
fp.close() | ||||
except IOError as err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
return | ||||
if not st: | ||||
return | ||||
parse_dirstate = util.nogc(self._rustmap.read) | ||||
parents = parse_dirstate(st) | ||||
if parents and not self._dirtyparents: | ||||
self.setparents(*parents) | ||||
|
r43574 | self.__contains__ = self._rustmap.__contains__ | ||
self.__getitem__ = self._rustmap.__getitem__ | ||||
self.get = self._rustmap.get | ||||
|
r43000 | def write(self, st, now): | ||
parents = self.parents() | ||||
st.write(self._rustmap.write(parents[0], parents[1], now)) | ||||
st.close() | ||||
self._dirtyparents = False | ||||
@propertycache | ||||
def filefoldmap(self): | ||||
"""Returns a dictionary mapping normalized case paths to their | ||||
non-normalized versions. | ||||
""" | ||||
return self._rustmap.filefoldmapasdict() | ||||
def hastrackeddir(self, d): | ||||
|
r43346 | self._dirs # Trigger Python's propertycache | ||
|
r43000 | return self._rustmap.hastrackeddir(d) | ||
def hasdir(self, d): | ||||
|
r43346 | self._dirs # Trigger Python's propertycache | ||
|
r43000 | return self._rustmap.hasdir(d) | ||
@propertycache | ||||
def _dirs(self): | ||||
return self._rustmap.getdirs() | ||||
@propertycache | ||||
def _alldirs(self): | ||||
return self._rustmap.getalldirs() | ||||
@propertycache | ||||
def identity(self): | ||||
self._rustmap | ||||
return self.identity | ||||
@property | ||||
def nonnormalset(self): | ||||
|
r44779 | nonnorm = self._rustmap.non_normal_entries() | ||
|
r43000 | return nonnorm | ||
|
r44719 | @propertycache | ||
|
r43000 | def otherparentset(self): | ||
|
r44779 | otherparents = self._rustmap.other_parent_entries() | ||
|
r43000 | return otherparents | ||
@propertycache | ||||
def dirfoldmap(self): | ||||
f = {} | ||||
normcase = util.normcase | ||||
for name in self._dirs: | ||||
f[normcase(name)] = name | ||||
return f | ||||