util.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. # This module is part of GitPython and is released under the
  2. # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
  3. """Index utilities."""
  4. __all__ = ["TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir"]
  5. import contextlib
  6. from functools import wraps
  7. import os
  8. import os.path as osp
  9. import struct
  10. import tempfile
  11. from types import TracebackType
  12. # typing ----------------------------------------------------------------------
  13. from typing import Any, Callable, TYPE_CHECKING, Optional, Type, cast
  14. from git.types import Literal, PathLike, _T
  15. if TYPE_CHECKING:
  16. from git.index import IndexFile
  17. # ---------------------------------------------------------------------------------
  18. # { Aliases
  19. pack = struct.pack
  20. unpack = struct.unpack
  21. # } END aliases
  22. class TemporaryFileSwap:
  23. """Utility class moving a file to a temporary location within the same directory and
  24. moving it back on to where on object deletion."""
  25. __slots__ = ("file_path", "tmp_file_path")
  26. def __init__(self, file_path: PathLike) -> None:
  27. self.file_path = file_path
  28. dirname, basename = osp.split(file_path)
  29. fd, self.tmp_file_path = tempfile.mkstemp(prefix=basename, dir=dirname)
  30. os.close(fd)
  31. with contextlib.suppress(OSError): # It may be that the source does not exist.
  32. os.replace(self.file_path, self.tmp_file_path)
  33. def __enter__(self) -> "TemporaryFileSwap":
  34. return self
  35. def __exit__(
  36. self,
  37. exc_type: Optional[Type[BaseException]],
  38. exc_val: Optional[BaseException],
  39. exc_tb: Optional[TracebackType],
  40. ) -> Literal[False]:
  41. if osp.isfile(self.tmp_file_path):
  42. os.replace(self.tmp_file_path, self.file_path)
  43. return False
  44. # { Decorators
  45. def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
  46. """Decorator for functions that alter the index using the git command.
  47. When a git command alters the index, this invalidates our possibly existing entries
  48. dictionary, which is why it must be deleted to allow it to be lazily reread later.
  49. """
  50. @wraps(func)
  51. def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
  52. rval = func(self, *args, **kwargs)
  53. self._delete_entries_cache()
  54. return rval
  55. # END wrapper method
  56. return post_clear_cache_if_not_raised
  57. def default_index(func: Callable[..., _T]) -> Callable[..., _T]:
  58. """Decorator ensuring the wrapped method may only run if we are the default
  59. repository index.
  60. This is as we rely on git commands that operate on that index only.
  61. """
  62. @wraps(func)
  63. def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
  64. if self._file_path != self._index_path():
  65. raise AssertionError(
  66. "Cannot call %r on indices that do not represent the default git index" % func.__name__
  67. )
  68. return func(self, *args, **kwargs)
  69. # END wrapper method
  70. return check_default_index
  71. def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
  72. """Decorator which changes the current working dir to the one of the git
  73. repository in order to ensure relative paths are handled correctly."""
  74. @wraps(func)
  75. def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
  76. cur_wd = os.getcwd()
  77. os.chdir(cast(PathLike, self.repo.working_tree_dir))
  78. try:
  79. return func(self, *args, **kwargs)
  80. finally:
  81. os.chdir(cur_wd)
  82. # END handle working dir
  83. # END wrapper
  84. return set_git_working_dir
  85. # } END decorators