dirstate.py
1537 lines
| 53.4 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 ( | ||
r48295 | dirstatemap, | |||
|
r27503 | encoding, | ||
error, | ||||
match as matchmod, | ||||
pathutil, | ||||
|
r32372 | policy, | ||
|
r30304 | pycompat, | ||
|
r27503 | scmutil, | ||
|
r45017 | sparse, | ||
|
r27503 | util, | ||
) | ||||
|
r1089 | |||
|
r43197 | from .interfaces import ( | ||
dirstate as intdirstate, | ||||
util as interfaceutil, | ||||
) | ||||
|
r43906 | parsers = policy.importmod('parsers') | ||
rustmod = policy.importrust('dirstate') | ||||
|
r32372 | |||
|
r48052 | SUPPORTS_DIRSTATE_V2 = rustmod is not None | ||
|
r8261 | propertycache = util.propertycache | ||
|
r16201 | filecache = scmutil.filecache | ||
r48310 | _rangemask = dirstatemap.rangemask | |||
|
r16201 | |||
|
r48858 | DirstateItem = dirstatemap.DirstateItem | ||
|
r21808 | |||
r48276 | ||||
|
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 | |||
r48392 | def requires_parents_change(func): | |||
def wrap(self, *args, **kwargs): | ||||
if not self.pendingparentchange(): | ||||
msg = 'calling `%s` outside of a parentchange context' | ||||
msg %= func.__name__ | ||||
raise error.ProgrammingError(msg) | ||||
return func(self, *args, **kwargs) | ||||
return wrap | ||||
r48393 | def requires_no_parents_change(func): | |||
def wrap(self, *args, **kwargs): | ||||
r48394 | if self.pendingparentchange(): | |||
r48393 | msg = 'calling `%s` inside of a parentchange context' | |||
msg %= func.__name__ | ||||
raise error.ProgrammingError(msg) | ||||
return func(self, *args, **kwargs) | ||||
return wrap | ||||
|
r43197 | @interfaceutil.implementer(intdirstate.idirstate) | ||
|
r1559 | class dirstate(object): | ||
|
r47538 | def __init__( | ||
|
r48055 | self, | ||
opener, | ||||
ui, | ||||
root, | ||||
validate, | ||||
sparsematchfn, | ||||
nodeconstants, | ||||
use_dirstate_v2, | ||||
|
r47538 | ): | ||
|
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 | """ | ||
|
r48055 | self._use_dirstate_v2 = use_dirstate_v2 | ||
|
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 | ||||
r48295 | self._mapcls = dirstatemap.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( | ||
|
r48055 | self._ui, | ||
self._opener, | ||||
self._root, | ||||
self._nodeconstants, | ||||
self._use_dirstate_v2, | ||||
|
r47538 | ) | ||
|
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 | ||||
r48301 | ||||
XXX The "state" is a bit obscure to be in the "public" API. we should | ||||
consider migrating all user of this to going through the dirstate entry | ||||
instead. | ||||
|
r46554 | """ | ||
r48919 | msg = b"don't use dirstate[file], use dirstate.get_entry(file)" | |||
util.nouideprecwarn(msg, b'6.1', stacklevel=2) | ||||
r48301 | entry = self._map.get(key) | |||
if entry is not None: | ||||
return entry.state | ||||
return b'?' | ||||
|
r1089 | |||
r48897 | def get_entry(self, path): | |||
"""return a DirstateItem for the associated path""" | ||||
entry = self._map.get(path) | ||||
if entry is None: | ||||
return DirstateItem() | ||||
return entry | ||||
|
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]) | ||||
r48299 | @property | |||
def in_merge(self): | ||||
"""True if a merge is in progress""" | ||||
return self._pl[1] != self._nodeconstants.nullid | ||||
|
r4179 | def branch(self): | ||
|
r13047 | return encoding.tolocal(self._branch) | ||
|
r4179 | |||
|
r47771 | def setparents(self, p1, p2=None): | ||
|
r16551 | """Set dirstate parents to p1 and p2. | ||
r48302 | When moving from two parents to one, "merged" entries a | |||
|
r16551 | adjusted to normal and previous copy records discarded and | ||
returned by the call. | ||||
See localrepo.setparents() | ||||
""" | ||||
|
r47771 | 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 | ||||
r48801 | nullid = self._nodeconstants.nullid | |||
r48873 | # True if we need to fold p2 related state back to a linear case | |||
fold_p2 = oldp2 != nullid and p2 == nullid | ||||
return self._map.setparents(p1, p2, fold_p2=fold_p2) | ||||
|
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 | ||
|
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 | ||
r48871 | else: | |||
self._map.copymap.pop(dest, None) | ||||
|
r1089 | |||
def copied(self, file): | ||||
|
r34337 | return self._map.copymap.get(file, None) | ||
|
r3154 | |||
def copies(self): | ||||
|
r34337 | return self._map.copymap | ||
|
r1089 | |||
r48393 | @requires_no_parents_change | |||
def set_tracked(self, filename): | ||||
"""a "public" method for generic code to mark a file as tracked | ||||
This function is to be called outside of "update/merge" case. For | ||||
example by a command like `hg add X`. | ||||
return True the file was previously untracked, False otherwise. | ||||
""" | ||||
r48798 | self._dirty = True | |||
r48393 | entry = self._map.get(filename) | |||
r48804 | if entry is None or not entry.tracked: | |||
r48798 | self._check_new_tracked_filename(filename) | |||
r48804 | return self._map.set_tracked(filename) | |||
r48393 | ||||
r48399 | @requires_no_parents_change | |||
def set_untracked(self, filename): | ||||
"""a "public" method for generic code to mark a file as untracked | ||||
This function is to be called outside of "update/merge" case. For | ||||
example by a command like `hg remove X`. | ||||
return True the file was previously tracked, False otherwise. | ||||
""" | ||||
r48786 | ret = self._map.set_untracked(filename) | |||
if ret: | ||||
r48669 | self._dirty = True | |||
r48786 | return ret | |||
r48399 | ||||
r48504 | @requires_no_parents_change | |||
def set_clean(self, filename, parentfiledata=None): | ||||
"""record that the current state of the file on disk is known to be clean""" | ||||
self._dirty = True | ||||
r48728 | if parentfiledata: | |||
(mode, size, mtime) = parentfiledata | ||||
else: | ||||
(mode, size, mtime) = self._get_filedata(filename) | ||||
r48788 | if not self._map[filename].tracked: | |||
self._check_new_tracked_filename(filename) | ||||
self._map.set_clean(filename, mode, size, mtime) | ||||
r48728 | if mtime > self._lastnormaltime: | |||
# Remember the most recent modification timeslot for status(), | ||||
# to make sure we won't miss future size-preserving file content | ||||
# modifications that happen within the same timeslot. | ||||
self._lastnormaltime = mtime | ||||
r48504 | ||||
r48520 | @requires_no_parents_change | |||
def set_possibly_dirty(self, filename): | ||||
"""record that the current state of the file on disk is unknown""" | ||||
self._dirty = True | ||||
self._map.set_possibly_dirty(filename) | ||||
r48392 | @requires_parents_change | |||
r48493 | def update_file_p1( | |||
r48392 | self, | |||
filename, | ||||
p1_tracked, | ||||
): | ||||
"""Set a file as tracked in the parent (or not) | ||||
This is to be called when adjust the dirstate to a new parent after an history | ||||
rewriting operation. | ||||
It should not be called during a merge (p2 != nullid) and only within | ||||
a `with dirstate.parentchange():` context. | ||||
""" | ||||
if self.in_merge: | ||||
msg = b'update_file_reference should not be called when merging' | ||||
raise error.ProgrammingError(msg) | ||||
entry = self._map.get(filename) | ||||
if entry is None: | ||||
wc_tracked = False | ||||
else: | ||||
wc_tracked = entry.tracked | ||||
r48952 | if not (p1_tracked or wc_tracked): | |||
r48392 | # the file is no longer relevant to anyone | |||
r48813 | if self._map.get(filename) is not None: | |||
self._map.reset_state(filename) | ||||
r48802 | self._dirty = True | |||
r48392 | elif (not p1_tracked) and wc_tracked: | |||
r48494 | if entry is not None and entry.added: | |||
return # avoid dropping copy information (maybe?) | ||||
r48392 | ||||
r48494 | # this mean we are doing call for file we do not really care about the | |||
# data (eg: added or removed), however this should be a minor overhead | ||||
# compared to the overall update process calling this. | ||||
parentfiledata = None | ||||
if wc_tracked: | ||||
parentfiledata = self._get_filedata(filename) | ||||
self._map.reset_state( | ||||
filename, | ||||
wc_tracked, | ||||
p1_tracked, | ||||
r48952 | # the underlying reference might have changed, we will have to | |||
# check it. | ||||
has_meaningful_mtime=False, | ||||
r48494 | parentfiledata=parentfiledata, | |||
) | ||||
r48496 | if ( | |||
parentfiledata is not None | ||||
and parentfiledata[2] > self._lastnormaltime | ||||
): | ||||
# Remember the most recent modification timeslot for status(), | ||||
# to make sure we won't miss future size-preserving file content | ||||
# modifications that happen within the same timeslot. | ||||
self._lastnormaltime = parentfiledata[2] | ||||
r48494 | ||||
r48411 | @requires_parents_change | |||
def update_file( | ||||
self, | ||||
filename, | ||||
wc_tracked, | ||||
p1_tracked, | ||||
p2_tracked=False, | ||||
merged=False, | ||||
clean_p1=False, | ||||
clean_p2=False, | ||||
possibly_dirty=False, | ||||
r48491 | parentfiledata=None, | |||
r48411 | ): | |||
"""update the information about a file in the dirstate | ||||
This is to be called when the direstates parent changes to keep track | ||||
of what is the file situation in regards to the working copy and its parent. | ||||
This function must be called within a `dirstate.parentchange` context. | ||||
|
r48570 | note: the API is at an early stage and we might need to adjust it | ||
r48411 | depending of what information ends up being relevant and useful to | |||
other processing. | ||||
""" | ||||
if merged and (clean_p1 or clean_p2): | ||||
msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`' | ||||
raise error.ProgrammingError(msg) | ||||
r48492 | ||||
# note: I do not think we need to double check name clash here since we | ||||
# are in a update/merge case that should already have taken care of | ||||
# this. The test agrees | ||||
self._dirty = True | ||||
need_parent_file_data = ( | ||||
not (possibly_dirty or clean_p2 or merged) | ||||
and wc_tracked | ||||
and p1_tracked | ||||
) | ||||
# this mean we are doing call for file we do not really care about the | ||||
# data (eg: added or removed), however this should be a minor overhead | ||||
# compared to the overall update process calling this. | ||||
if need_parent_file_data: | ||||
if parentfiledata is None: | ||||
parentfiledata = self._get_filedata(filename) | ||||
mtime = parentfiledata[2] | ||||
if mtime > self._lastnormaltime: | ||||
# Remember the most recent modification timeslot for | ||||
# status(), to make sure we won't miss future | ||||
# size-preserving file content modifications that happen | ||||
# within the same timeslot. | ||||
self._lastnormaltime = mtime | ||||
self._map.reset_state( | ||||
filename, | ||||
wc_tracked, | ||||
p1_tracked, | ||||
r48952 | p2_info=merged or clean_p2, | |||
has_meaningful_mtime=not possibly_dirty, | ||||
r48492 | parentfiledata=parentfiledata, | |||
) | ||||
r48495 | if ( | |||
parentfiledata is not None | ||||
and parentfiledata[2] > self._lastnormaltime | ||||
): | ||||
# Remember the most recent modification timeslot for status(), | ||||
# to make sure we won't miss future size-preserving file content | ||||
# modifications that happen within the same timeslot. | ||||
self._lastnormaltime = parentfiledata[2] | ||||
r48411 | ||||
r48787 | def _check_new_tracked_filename(self, filename): | |||
scmutil.checkfilename(filename) | ||||
if self._map.hastrackeddir(filename): | ||||
msg = _(b'directory %r already in dirstate') | ||||
msg %= pycompat.bytestr(filename) | ||||
raise error.Abort(msg) | ||||
# shadows | ||||
for d in pathutil.finddirs(filename): | ||||
if self._map.hastrackeddir(d): | ||||
break | ||||
entry = self._map.get(d) | ||||
if entry is not None and not entry.removed: | ||||
msg = _(b'file %r in dirstate clashes with %r') | ||||
msg %= (pycompat.bytestr(d), pycompat.bytestr(filename)) | ||||
raise error.Abort(msg) | ||||
r48490 | def _get_filedata(self, filename): | |||
"""returns""" | ||||
s = os.lstat(self._join(filename)) | ||||
mode = s.st_mode | ||||
size = s.st_size | ||||
mtime = s[stat.ST_MTIME] | ||||
return (mode, size, mtime) | ||||
|
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 | ||
|
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 | ||||
|
r47771 | self._map.setparents(parent, self._nodeconstants.nullid) | ||
|
r44343 | |||
for f in to_lookup: | ||||
r48806 | ||||
if self.in_merge: | ||||
self.set_tracked(f) | ||||
else: | ||||
self._map.reset_state( | ||||
f, | ||||
wc_tracked=True, | ||||
p1_tracked=True, | ||||
) | ||||
|
r44343 | for f in to_drop: | ||
r48814 | self._map.reset_state(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 | ||||
r48869 | # record when mtime start to be ambiguous | |||
|
r26747 | now = _getfsnow(self._opener) | ||
|
r26634 | |||
# delay writing in-memory changes out | ||||
|
r43346 | tr.addfilegenerator( | ||
|
r43347 | b'dirstate', | ||
|
r43346 | (self._filename,), | ||
r48869 | lambda f: self._writedirstate(tr, f, now=now), | |||
|
r43347 | location=b'plain', | ||
|
r43346 | ) | ||
|
r26634 | return | ||
|
r43347 | st = self._opener(filename, b"w", atomictemp=True, checkambig=True) | ||
|
r48474 | self._writedirstate(tr, st) | ||
|
r26521 | |||
|
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 | ||||
r48869 | def _writedirstate(self, tr, st, now=None): | |||
|
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 | ||||
r48869 | ||||
if now is None: | ||||
# use the modification time of the newly created temporary file as the | ||||
# filesystem's notion of 'now' | ||||
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): | ||
r48321 | if e.need_delay(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 | |||
|
r48474 | self._map.write(tr, 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, | ||
|
r48139 | dirty, | ||
|
r44537 | ) = rustmod.status( | ||
r48933 | self._map._map, | |||
|
r44537 | 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 | |||
|
r48139 | self._dirty |= dirty | ||
|
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) | ||
r48325 | mode = t.mode | |||
r48326 | size = t.size | |||
r48327 | time = t.mtime | |||
|
r6591 | |||
r48320 | if not st and t.tracked: | |||
|
r6818 | dadd(fn) | ||
r48322 | elif t.merged: | |||
madd(fn) | ||||
elif t.added: | ||||
aadd(fn) | ||||
elif t.removed: | ||||
radd(fn) | ||||
r48323 | elif t.tracked: | |||
|
r43346 | if ( | ||
size >= 0 | ||||
and ( | ||||
(size != st.st_size and size != st.st_size & _rangemask) | ||||
or ((mode ^ st.st_mode) & 0o100 and checkexec) | ||||
) | ||||
r48303 | or t.from_p2 | |||
|
r43346 | 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) | ||
|
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: | ||
r48933 | dmap = self._map._map | |||
|
r44833 | |||
|
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( | ||
|
r48474 | tr, | ||
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,), | ||
|
r48474 | lambda f: self._writedirstate(tr, f), | ||
|
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) | ||||
r48901 | ||||
def verify(self, m1, m2): | ||||
"""check the dirstate content again the parent manifest and yield errors""" | ||||
missing_from_p1 = b"%s in state %s, but not in manifest1\n" | ||||
unexpected_in_p1 = b"%s in state %s, but also in manifest1\n" | ||||
missing_from_ps = b"%s in state %s, but not in either manifest\n" | ||||
missing_from_ds = b"%s in manifest1, but listed as state %s\n" | ||||
for f, entry in self.items(): | ||||
state = entry.state | ||||
if state in b"nr" and f not in m1: | ||||
yield (missing_from_p1, f, state) | ||||
if state in b"a" and f in m1: | ||||
yield (unexpected_in_p1, f, state) | ||||
if state in b"m" and f not in m1 and f not in m2: | ||||
yield (missing_from_ps, f, state) | ||||
for f in m1: | ||||
state = self.get_entry(f).state | ||||
if state not in b"nrm": | ||||
yield (missing_from_ds, f, state) | ||||