mercurial/vfs.py
changeset 51888 fa9e8a6521c1
parent 51887 ad83e4f9b40e
child 51889 22e1924e9402
equal deleted inserted replaced
51887:ad83e4f9b40e 51888:fa9e8a6521c1
    15 import threading
    15 import threading
    16 import typing
    16 import typing
    17 
    17 
    18 from typing import (
    18 from typing import (
    19     Any,
    19     Any,
       
    20     BinaryIO,
       
    21     Callable,
    20     Iterable,
    22     Iterable,
    21     Iterator,
    23     Iterator,
    22     List,
    24     List,
       
    25     MutableMapping,
    23     Optional,
    26     Optional,
    24     Tuple,
    27     Tuple,
    25     Type,
    28     Type,
    26     TypeVar,
    29     TypeVar,
    27 )
    30 )
    34     pycompat,
    37     pycompat,
    35     util,
    38     util,
    36 )
    39 )
    37 
    40 
    38 if typing.TYPE_CHECKING:
    41 if typing.TYPE_CHECKING:
       
    42     from . import (
       
    43         ui as uimod,
       
    44     )
       
    45 
    39     _Tbackgroundfilecloser = TypeVar(
    46     _Tbackgroundfilecloser = TypeVar(
    40         '_Tbackgroundfilecloser', bound='backgroundfilecloser'
    47         '_Tbackgroundfilecloser', bound='backgroundfilecloser'
    41     )
    48     )
    42     _Tclosewrapbase = TypeVar('_Tclosewrapbase', bound='closewrapbase')
    49     _Tclosewrapbase = TypeVar('_Tclosewrapbase', bound='closewrapbase')
    43 
    50     _OnErrorFn = Callable[[Exception], Optional[object]]
    44 
    51 
    45 def _avoidambig(path: bytes, oldstat) -> None:
    52 
       
    53 def _avoidambig(path: bytes, oldstat: util.filestat) -> None:
    46     """Avoid file stat ambiguity forcibly
    54     """Avoid file stat ambiguity forcibly
    47 
    55 
    48     This function causes copying ``path`` file, if it is owned by
    56     This function causes copying ``path`` file, if it is owned by
    49     another (see issue5418 and issue5584 for detail).
    57     another (see issue5418 and issue5584 for detail).
    50     """
    58     """
    76     @abc.abstractmethod
    84     @abc.abstractmethod
    77     def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs) -> Any:
    85     def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs) -> Any:
    78         ...
    86         ...
    79 
    87 
    80     @abc.abstractmethod
    88     @abc.abstractmethod
    81     def _auditpath(self, path: bytes, mode: bytes) -> Any:
    89     def _auditpath(self, path: bytes, mode: bytes) -> None:
    82         ...
    90         ...
    83 
    91 
    84     @abc.abstractmethod
    92     @abc.abstractmethod
    85     def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
    93     def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
    86         ...
    94         ...
    91             return self.read(path)
    99             return self.read(path)
    92         except FileNotFoundError:
   100         except FileNotFoundError:
    93             pass
   101             pass
    94         return b""
   102         return b""
    95 
   103 
    96     def tryreadlines(self, path: bytes, mode: bytes = b'rb') -> Any:
   104     def tryreadlines(self, path: bytes, mode: bytes = b'rb') -> List[bytes]:
    97         '''gracefully return an empty array for missing files'''
   105         '''gracefully return an empty array for missing files'''
    98         try:
   106         try:
    99             return self.readlines(path, mode=mode)
   107             return self.readlines(path, mode=mode)
   100         except FileNotFoundError:
   108         except FileNotFoundError:
   101             pass
   109             pass
   113 
   121 
   114     def read(self, path: bytes) -> bytes:
   122     def read(self, path: bytes) -> bytes:
   115         with self(path, b'rb') as fp:
   123         with self(path, b'rb') as fp:
   116             return fp.read()
   124             return fp.read()
   117 
   125 
   118     def readlines(self, path: bytes, mode: bytes = b'rb') -> Any:
   126     def readlines(self, path: bytes, mode: bytes = b'rb') -> List[bytes]:
   119         with self(path, mode=mode) as fp:
   127         with self(path, mode=mode) as fp:
   120             return fp.readlines()
   128             return fp.readlines()
   121 
   129 
   122     def write(
   130     def write(
   123         self, path: bytes, data: bytes, backgroundclose=False, **kwargs
   131         self, path: bytes, data: bytes, backgroundclose: bool = False, **kwargs
   124     ) -> int:
   132     ) -> int:
   125         with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
   133         with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
   126             return fp.write(data)
   134             return fp.write(data)
   127 
   135 
   128     def writelines(
   136     def writelines(
   129         self,
   137         self,
   130         path: bytes,
   138         path: bytes,
   131         data: Iterable[bytes],
   139         data: Iterable[bytes],
   132         mode: bytes = b'wb',
   140         mode: bytes = b'wb',
   133         notindexed=False,
   141         notindexed: bool = False,
   134     ) -> None:
   142     ) -> None:
   135         with self(path, mode=mode, notindexed=notindexed) as fp:
   143         with self(path, mode=mode, notindexed=notindexed) as fp:
   136             return fp.writelines(data)
   144             return fp.writelines(data)
   137 
   145 
   138     def append(self, path: bytes, data: bytes) -> int:
   146     def append(self, path: bytes, data: bytes) -> int:
   155         return os.path.dirname(path)
   163         return os.path.dirname(path)
   156 
   164 
   157     def exists(self, path: Optional[bytes] = None) -> bool:
   165     def exists(self, path: Optional[bytes] = None) -> bool:
   158         return os.path.exists(self.join(path))
   166         return os.path.exists(self.join(path))
   159 
   167 
   160     def fstat(self, fp) -> os.stat_result:
   168     def fstat(self, fp: BinaryIO) -> os.stat_result:
   161         return util.fstat(fp)
   169         return util.fstat(fp)
   162 
   170 
   163     def isdir(self, path: Optional[bytes] = None) -> bool:
   171     def isdir(self, path: Optional[bytes] = None) -> bool:
   164         return os.path.isdir(self.join(path))
   172         return os.path.isdir(self.join(path))
   165 
   173 
   247     def makedirs(
   255     def makedirs(
   248         self, path: Optional[bytes] = None, mode: Optional[int] = None
   256         self, path: Optional[bytes] = None, mode: Optional[int] = None
   249     ) -> None:
   257     ) -> None:
   250         return util.makedirs(self.join(path), mode)
   258         return util.makedirs(self.join(path), mode)
   251 
   259 
   252     def makelock(self, info, path: bytes) -> None:
   260     def makelock(self, info: bytes, path: bytes) -> None:
   253         return util.makelock(info, self.join(path))
   261         return util.makelock(info, self.join(path))
   254 
   262 
   255     def mkdir(self, path: Optional[bytes] = None) -> None:
   263     def mkdir(self, path: Optional[bytes] = None) -> None:
   256         return os.mkdir(self.join(path))
   264         return os.mkdir(self.join(path))
   257 
   265 
   268         if dir:
   276         if dir:
   269             return fd, os.path.join(dir, fname)
   277             return fd, os.path.join(dir, fname)
   270         else:
   278         else:
   271             return fd, fname
   279             return fd, fname
   272 
   280 
       
   281     # TODO: This doesn't match osutil.listdir().  stat=False in pure;
       
   282     #  non-optional bool in cext.  'skip' is bool if we trust cext, or bytes
       
   283     #  going by how pure uses it.  Also, cext returns a custom stat structure.
       
   284     #  from cext.osutil.pyi:
       
   285     #
       
   286     #     path: bytes, st: bool, skip: Optional[bool]
   273     def readdir(
   287     def readdir(
   274         self, path: Optional[bytes] = None, stat=None, skip=None
   288         self, path: Optional[bytes] = None, stat=None, skip=None
   275     ) -> Any:
   289     ) -> Any:
   276         return util.listdir(self.join(path), stat, skip)
   290         return util.listdir(self.join(path), stat, skip)
   277 
   291 
   278     def readlock(self, path: bytes) -> bytes:
   292     def readlock(self, path: bytes) -> bytes:
   279         return util.readlock(self.join(path))
   293         return util.readlock(self.join(path))
   280 
   294 
   281     def rename(self, src: bytes, dst: bytes, checkambig=False) -> None:
   295     def rename(self, src: bytes, dst: bytes, checkambig: bool = False) -> None:
   282         """Rename from src to dst
   296         """Rename from src to dst
   283 
   297 
   284         checkambig argument is used with util.filestat, and is useful
   298         checkambig argument is used with util.filestat, and is useful
   285         only if destination file is guarded by any lock
   299         only if destination file is guarded by any lock
   286         (e.g. repo.lock or repo.wlock).
   300         (e.g. repo.lock or repo.wlock).
   310     def rmdir(self, path: Optional[bytes] = None) -> None:
   324     def rmdir(self, path: Optional[bytes] = None) -> None:
   311         """Remove an empty directory."""
   325         """Remove an empty directory."""
   312         return os.rmdir(self.join(path))
   326         return os.rmdir(self.join(path))
   313 
   327 
   314     def rmtree(
   328     def rmtree(
   315         self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False
   329         self,
       
   330         path: Optional[bytes] = None,
       
   331         ignore_errors: bool = False,
       
   332         forcibly: bool = False,
   316     ) -> None:
   333     ) -> None:
   317         """Remove a directory tree recursively
   334         """Remove a directory tree recursively
   318 
   335 
   319         If ``forcibly``, this tries to remove READ-ONLY files, too.
   336         If ``forcibly``, this tries to remove READ-ONLY files, too.
   320         """
   337         """
   321         if forcibly:
   338         if forcibly:
   322 
   339 
   323             def onexc(function, path, excinfo):
   340             def onexc(function, path: bytes, excinfo):
   324                 if function is not os.remove:
   341                 if function is not os.remove:
   325                     raise
   342                     raise
   326                 # read-only files cannot be unlinked under Windows
   343                 # read-only files cannot be unlinked under Windows
   327                 s = os.stat(path)
   344                 s = os.stat(path)
   328                 if (s.st_mode & stat.S_IWRITE) != 0:
   345                 if (s.st_mode & stat.S_IWRITE) != 0:
   355     def tryunlink(self, path: Optional[bytes] = None) -> bool:
   372     def tryunlink(self, path: Optional[bytes] = None) -> bool:
   356         """Attempt to remove a file, ignoring missing file errors."""
   373         """Attempt to remove a file, ignoring missing file errors."""
   357         return util.tryunlink(self.join(path))
   374         return util.tryunlink(self.join(path))
   358 
   375 
   359     def unlinkpath(
   376     def unlinkpath(
   360         self, path: Optional[bytes] = None, ignoremissing=False, rmdir=True
   377         self,
       
   378         path: Optional[bytes] = None,
       
   379         ignoremissing: bool = False,
       
   380         rmdir: bool = True,
   361     ) -> None:
   381     ) -> None:
   362         return util.unlinkpath(
   382         return util.unlinkpath(
   363             self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
   383             self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
   364         )
   384         )
   365 
   385 
   366     def utime(self, path: Optional[bytes] = None, t=None) -> None:
   386     # TODO: could be Tuple[float, float] too.
       
   387     def utime(
       
   388         self, path: Optional[bytes] = None, t: Optional[Tuple[int, int]] = None
       
   389     ) -> None:
   367         return os.utime(self.join(path), t)
   390         return os.utime(self.join(path), t)
   368 
   391 
   369     def walk(
   392     def walk(
   370         self, path: Optional[bytes] = None, onerror=None
   393         self, path: Optional[bytes] = None, onerror: Optional[_OnErrorFn] = None
   371     ) -> Iterator[Tuple[bytes, List[bytes], List[bytes]]]:
   394     ) -> Iterator[Tuple[bytes, List[bytes], List[bytes]]]:
   372         """Yield (dirpath, dirs, files) tuple for each directory under path
   395         """Yield (dirpath, dirs, files) tuple for each directory under path
   373 
   396 
   374         ``dirpath`` is relative one from the root of this vfs. This
   397         ``dirpath`` is relative one from the root of this vfs. This
   375         uses ``os.sep`` as path separator, even you specify POSIX
   398         uses ``os.sep`` as path separator, even you specify POSIX
   384         for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
   407         for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
   385             yield (dirpath[prefixlen:], dirs, files)
   408             yield (dirpath[prefixlen:], dirs, files)
   386 
   409 
   387     @contextlib.contextmanager
   410     @contextlib.contextmanager
   388     def backgroundclosing(
   411     def backgroundclosing(
   389         self, ui, expectedcount=-1
   412         self, ui: uimod.ui, expectedcount: int = -1
   390     ) -> Iterator[Optional[backgroundfilecloser]]:
   413     ) -> Iterator[Optional[backgroundfilecloser]]:
   391         """Allow files to be closed asynchronously.
   414         """Allow files to be closed asynchronously.
   392 
   415 
   393         When this context manager is active, ``backgroundclose`` can be passed
   416         When this context manager is active, ``backgroundclose`` can be passed
   394         to ``__call__``/``open`` to result in the file possibly being closed
   417         to ``__call__``/``open`` to result in the file possibly being closed
   415             finally:
   438             finally:
   416                 vfs._backgroundfilecloser = (
   439                 vfs._backgroundfilecloser = (
   417                     None  # pytype: disable=attribute-error
   440                     None  # pytype: disable=attribute-error
   418                 )
   441                 )
   419 
   442 
   420     def register_file(self, path) -> None:
   443     def register_file(self, path: bytes) -> None:
   421         """generic hook point to lets fncache steer its stew"""
   444         """generic hook point to lets fncache steer its stew"""
   422 
   445 
   423 
   446 
   424 class vfs(abstractvfs):
   447 class vfs(abstractvfs):
   425     """Operate files relative to a base directory
   448     """Operate files relative to a base directory
   430     'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
   453     'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
   431     (b) the base directory is managed by hg and considered sort-of append-only.
   454     (b) the base directory is managed by hg and considered sort-of append-only.
   432     See pathutil.pathauditor() for details.
   455     See pathutil.pathauditor() for details.
   433     """
   456     """
   434 
   457 
       
   458     createmode: Optional[int]
       
   459 
   435     def __init__(
   460     def __init__(
   436         self,
   461         self,
   437         base: bytes,
   462         base: bytes,
   438         audit=True,
   463         audit: bool = True,
   439         cacheaudited=False,
   464         cacheaudited: bool = False,
   440         expandpath=False,
   465         expandpath: bool = False,
   441         realpath=False,
   466         realpath: bool = False,
   442     ) -> None:
   467     ) -> None:
   443         if expandpath:
   468         if expandpath:
   444             base = util.expandpath(base)
   469             base = util.expandpath(base)
   445         if realpath:
   470         if realpath:
   446             base = os.path.realpath(base)
   471             base = os.path.realpath(base)
   457     @util.propertycache
   482     @util.propertycache
   458     def _cansymlink(self) -> bool:
   483     def _cansymlink(self) -> bool:
   459         return util.checklink(self.base)
   484         return util.checklink(self.base)
   460 
   485 
   461     @util.propertycache
   486     @util.propertycache
   462     def _chmod(self):
   487     def _chmod(self) -> bool:
   463         return util.checkexec(self.base)
   488         return util.checkexec(self.base)
   464 
   489 
   465     def _fixfilemode(self, name) -> None:
   490     def _fixfilemode(self, name: bytes) -> None:
   466         if self.createmode is None or not self._chmod:
   491         if self.createmode is None or not self._chmod:
   467             return
   492             return
   468         os.chmod(name, self.createmode & 0o666)
   493         os.chmod(name, self.createmode & 0o666)
   469 
   494 
   470     def _auditpath(self, path, mode) -> None:
   495     def _auditpath(self, path: bytes, mode: bytes) -> None:
   471         if self._audit:
   496         if self._audit:
   472             if os.path.isabs(path) and path.startswith(self.base):
   497             if os.path.isabs(path) and path.startswith(self.base):
   473                 path = os.path.relpath(path, self.base)
   498                 path = os.path.relpath(path, self.base)
   474             r = util.checkosfilename(path)
   499             r = util.checkosfilename(path)
   475             if r:
   500             if r:
   476                 raise error.Abort(b"%s: %r" % (r, path))
   501                 raise error.Abort(b"%s: %r" % (r, path))
   477             self.audit(path, mode=mode)
   502             self.audit(path, mode=mode)
   478 
   503 
   479     def isfileorlink_checkdir(
   504     def isfileorlink_checkdir(
   480         self, dircache, path: Optional[bytes] = None
   505         self,
       
   506         dircache: MutableMapping[bytes, bool],
       
   507         path: Optional[bytes] = None,
   481     ) -> bool:
   508     ) -> bool:
   482         """return True if the path is a regular file or a symlink and
   509         """return True if the path is a regular file or a symlink and
   483         the directories along the path are "normal", that is
   510         the directories along the path are "normal", that is
   484         not symlinks or nested hg repositories.
   511         not symlinks or nested hg repositories.
   485 
   512 
   486         Ignores the `_audit` setting, and checks the directories regardless.
   513         Ignores the `_audit` setting, and checks the directories regardless.
   487         `dircache` is used to cache the directory checks.
   514         `dircache` is used to cache the directory checks.
   488         """
   515         """
       
   516         # TODO: Should be a None check on 'path', or shouldn't default to None
       
   517         #  because of the immediate call to util.localpath().
   489         try:
   518         try:
   490             for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)):
   519             for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)):
   491                 if prefix in dircache:
   520                 if prefix in dircache:
   492                     res = dircache[prefix]
   521                     res = dircache[prefix]
   493                 else:
   522                 else:
   503 
   532 
   504     def __call__(
   533     def __call__(
   505         self,
   534         self,
   506         path: bytes,
   535         path: bytes,
   507         mode: bytes = b"rb",
   536         mode: bytes = b"rb",
   508         atomictemp=False,
   537         atomictemp: bool = False,
   509         notindexed=False,
   538         notindexed: bool = False,
   510         backgroundclose=False,
   539         backgroundclose: bool = False,
   511         checkambig=False,
   540         checkambig: bool = False,
   512         auditpath=True,
   541         auditpath: bool = True,
   513         makeparentdirs=True,
   542         makeparentdirs: bool = True,
   514     ) -> Any:
   543     ) -> Any:  # TODO: should be BinaryIO if util.atomictempfile can be coersed
   515         """Open ``path`` file, which is relative to vfs root.
   544         """Open ``path`` file, which is relative to vfs root.
   516 
   545 
   517         By default, parent directories are created as needed. Newly created
   546         By default, parent directories are created as needed. Newly created
   518         directories are marked as "not to be indexed by the content indexing
   547         directories are marked as "not to be indexed by the content indexing
   519         service", if ``notindexed`` is specified for "write" mode access.
   548         service", if ``notindexed`` is specified for "write" mode access.
   648 
   677 
   649 opener: Type[vfs] = vfs
   678 opener: Type[vfs] = vfs
   650 
   679 
   651 
   680 
   652 class proxyvfs(abstractvfs, abc.ABC):
   681 class proxyvfs(abstractvfs, abc.ABC):
   653     def __init__(self, vfs: "vfs"):
   682     def __init__(self, vfs: vfs) -> None:
   654         self.vfs = vfs
   683         self.vfs = vfs
   655 
   684 
   656     @property
   685     @property
   657     def createmode(self):
   686     def createmode(self) -> Optional[int]:
   658         return self.vfs.createmode
   687         return self.vfs.createmode
   659 
   688 
   660     def _auditpath(self, path, mode) -> None:
   689     def _auditpath(self, path: bytes, mode: bytes) -> None:
   661         return self.vfs._auditpath(path, mode)
   690         return self.vfs._auditpath(path, mode)
   662 
   691 
   663     @property
   692     @property
   664     def options(self):
   693     def options(self):
   665         return self.vfs.options
   694         return self.vfs.options
   674 
   703 
   675 
   704 
   676 class filtervfs(proxyvfs, abstractvfs):
   705 class filtervfs(proxyvfs, abstractvfs):
   677     '''Wrapper vfs for filtering filenames with a function.'''
   706     '''Wrapper vfs for filtering filenames with a function.'''
   678 
   707 
   679     def __init__(self, vfs: "vfs", filter):
   708     def __init__(self, vfs: vfs, filter) -> None:
   680         proxyvfs.__init__(self, vfs)
   709         proxyvfs.__init__(self, vfs)
   681         self._filter = filter
   710         self._filter = filter
   682 
   711 
       
   712     # TODO: The return type should be BinaryIO
   683     def __call__(self, path: bytes, *args, **kwargs) -> Any:
   713     def __call__(self, path: bytes, *args, **kwargs) -> Any:
   684         return self.vfs(self._filter(path), *args, **kwargs)
   714         return self.vfs(self._filter(path), *args, **kwargs)
   685 
   715 
   686     def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
   716     def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
   687         if path:
   717         if path:
   694 
   724 
   695 
   725 
   696 class readonlyvfs(proxyvfs):
   726 class readonlyvfs(proxyvfs):
   697     '''Wrapper vfs preventing any writing.'''
   727     '''Wrapper vfs preventing any writing.'''
   698 
   728 
   699     def __init__(self, vfs: "vfs"):
   729     def __init__(self, vfs: vfs) -> None:
   700         proxyvfs.__init__(self, vfs)
   730         proxyvfs.__init__(self, vfs)
   701 
   731 
       
   732     # TODO: The return type should be BinaryIO
   702     def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw) -> Any:
   733     def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw) -> Any:
   703         if mode not in (b'r', b'rb'):
   734         if mode not in (b'r', b'rb'):
   704             raise error.Abort(_(b'this vfs is read only'))
   735             raise error.Abort(_(b'this vfs is read only'))
   705         return self.vfs(path, mode, *args, **kw)
   736         return self.vfs(path, mode, *args, **kw)
   706 
   737 
   715     """
   746     """
   716 
   747 
   717     def __init__(self, fh) -> None:
   748     def __init__(self, fh) -> None:
   718         object.__setattr__(self, '_origfh', fh)
   749         object.__setattr__(self, '_origfh', fh)
   719 
   750 
   720     def __getattr__(self, attr) -> Any:
   751     def __getattr__(self, attr: str) -> Any:
   721         return getattr(self._origfh, attr)
   752         return getattr(self._origfh, attr)
   722 
   753 
   723     def __setattr__(self, attr, value) -> None:
   754     def __setattr__(self, attr: str, value: Any) -> None:
   724         return setattr(self._origfh, attr, value)
   755         return setattr(self._origfh, attr, value)
   725 
   756 
   726     def __delattr__(self, attr) -> None:
   757     def __delattr__(self, attr: str) -> None:
   727         return delattr(self._origfh, attr)
   758         return delattr(self._origfh, attr)
   728 
   759 
   729     def __enter__(self: _Tclosewrapbase) -> _Tclosewrapbase:
   760     def __enter__(self: _Tclosewrapbase) -> _Tclosewrapbase:
   730         self._origfh.__enter__()
   761         self._origfh.__enter__()
   731         return self
   762         return self
   757 
   788 
   758 
   789 
   759 class backgroundfilecloser:
   790 class backgroundfilecloser:
   760     """Coordinates background closing of file handles on multiple threads."""
   791     """Coordinates background closing of file handles on multiple threads."""
   761 
   792 
   762     def __init__(self, ui, expectedcount=-1) -> None:
   793     def __init__(self, ui: uimod.ui, expectedcount: int = -1) -> None:
   763         self._running = False
   794         self._running = False
   764         self._entered = False
   795         self._entered = False
   765         self._threads = []
   796         self._threads = []
   766         self._threadexception = None
   797         self._threadexception = None
   767 
   798