| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,7 +4,7 @@ | |||
| 4 | 4 | # Required | |
| 5 | 5 | version: 2 | |
| 6 | 6 | ||
| 7 | - # Set the OS, Python version and other tools you might need | ||
| 7 | + # Set the OS, Python version and other tools you might need. | ||
| 8 | 8 | build: | |
| 9 | 9 | os: ubuntu-22.04 | |
| 10 | 10 | tools: | |
@@ -14,22 +14,24 @@ build: | |||
| 14 | 14 | # rust: "1.70" | |
| 15 | 15 | # golang: "1.20" | |
| 16 | 16 | ||
| 17 | - # Build documentation in the "docs/" directory with Sphinx | ||
| 17 | + # Build documentation in the "doc/" directory with Sphinx. | ||
| 18 | 18 | sphinx: | |
| 19 | 19 | configuration: doc/source/conf.py | |
| 20 | 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs | |
| 21 | 21 | # builder: "dirhtml" | |
| 22 | 22 | # Fail on all warnings to avoid broken references | |
| 23 | - # fail_on_warning: true | ||
| 23 | + fail_on_warning: true | ||
| 24 | 24 | ||
| 25 | - # Optionally build your docs in additional formats such as PDF and ePub | ||
| 25 | + # Optionally build your docs in additional formats such as PDF and ePub. | ||
| 26 | 26 | # formats: | |
| 27 | - # - pdf | ||
| 28 | - # - epub | ||
| 27 | + # - pdf | ||
| 28 | + # - epub | ||
| 29 | 29 | ||
| 30 | 30 | # Optional but recommended, declare the Python requirements required | |
| 31 | - # to build your documentation | ||
| 31 | + # to build your documentation. | ||
| 32 | 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html | |
| 33 | - # python: | ||
| 34 | - # install: | ||
| 35 | - # - requirements: docs/requirements.txt | ||
| 33 | + python: | ||
| 34 | + install: | ||
| 35 | + - method: pip | ||
| 36 | + path: . | ||
| 37 | + - requirements: doc/requirements.txt | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -93,7 +93,7 @@ | |||
| 93 | 93 | # Options for HTML output | |
| 94 | 94 | # ----------------------- | |
| 95 | 95 | ||
| 96 | - # html_theme = "sphinx_rtd_theme" | ||
| 96 | + html_theme = "sphinx_rtd_theme" | ||
| 97 | 97 | html_theme_options = {} | |
| 98 | 98 | ||
| 99 | 99 | # The name for this set of Sphinx documents. If None, it defaults to | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -120,7 +120,21 @@ | |||
| 120 | 120 | ||
| 121 | 121 | ||
| 122 | 122 | def refresh(path: Optional[PathLike] = None) -> None: | |
| 123 | - """Convenience method for setting the git executable path.""" | ||
| 123 | + """Convenience method for setting the git executable path. | ||
| 124 | + | ||
| 125 | + :param path: Optional path to the Git executable. If not absolute, it is resolved | ||
| 126 | + immediately, relative to the current directory. | ||
| 127 | + | ||
| 128 | + :note: The *path* parameter is usually omitted and cannot be used to specify a | ||
| 129 | + custom command whose location is looked up in a path search on each call. See | ||
| 130 | + :meth:`Git.refresh` for details on how to achieve this. | ||
| 131 | + | ||
| 132 | + :note: This calls :meth:`Git.refresh` and sets other global configuration according | ||
| 133 | + to the effect of doing so. As such, this function should usually be used instead | ||
| 134 | + of using :meth:`Git.refresh` or :meth:`FetchInfo.refresh` directly. | ||
| 135 | + | ||
| 136 | + :note: This function is called automatically, with no arguments, at import time. | ||
| 137 | + """ | ||
| 124 | 138 | global GIT_OK | |
| 125 | 139 | GIT_OK = False | |
| 126 | 140 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,6 +8,7 @@ | |||
| 8 | 8 | import re | |
| 9 | 9 | import contextlib | |
| 10 | 10 | import io | |
| 11 | + import itertools | ||
| 11 | 12 | import logging | |
| 12 | 13 | import os | |
| 13 | 14 | import signal | |
@@ -25,7 +26,6 @@ | |||
| 25 | 26 | UnsafeProtocolError, | |
| 26 | 27 | ) | |
| 27 | 28 | from git.util import ( | |
| 28 | - LazyMixin, | ||
| 29 | 29 | cygpath, | |
| 30 | 30 | expand_path, | |
| 31 | 31 | is_cygwin_git, | |
@@ -287,7 +287,7 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc | |||
| 287 | 287 | ## -- End Utilities -- @} | |
| 288 | 288 | ||
| 289 | 289 | ||
| 290 | - class Git(LazyMixin): | ||
| 290 | + class Git: | ||
| 291 | 291 | """The Git class manages communication with the Git binary. | |
| 292 | 292 | ||
| 293 | 293 | It provides a convenient interface to calling the Git binary, such as in:: | |
@@ -307,12 +307,18 @@ class Git(LazyMixin): | |||
| 307 | 307 | "cat_file_all", | |
| 308 | 308 | "cat_file_header", | |
| 309 | 309 | "_version_info", | |
| 310 | + "_version_info_token", | ||
| 310 | 311 | "_git_options", | |
| 311 | 312 | "_persistent_git_options", | |
| 312 | 313 | "_environment", | |
| 313 | 314 | ) | |
| 314 | 315 | ||
| 315 | - _excluded_ = ("cat_file_all", "cat_file_header", "_version_info") | ||
| 316 | + _excluded_ = ( | ||
| 317 | + "cat_file_all", | ||
| 318 | + "cat_file_header", | ||
| 319 | + "_version_info", | ||
| 320 | + "_version_info_token", | ||
| 321 | + ) | ||
| 316 | 322 | ||
| 317 | 323 | re_unsafe_protocol = re.compile(r"(.+)::.+") | |
| 318 | 324 | ||
@@ -344,6 +350,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: | |||
| 344 | 350 | for, which is not possible under most circumstances. | |
| 345 | 351 | ||
| 346 | 352 | See: | |
| 353 | + | ||
| 347 | 354 | - :meth:`Git.execute` (on the ``shell`` parameter). | |
| 348 | 355 | - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a | |
| 349 | 356 | - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags | |
@@ -355,13 +362,50 @@ def __setstate__(self, d: Dict[str, Any]) -> None: | |||
| 355 | 362 | GIT_PYTHON_GIT_EXECUTABLE = None | |
| 356 | 363 | """Provide the full path to the git executable. Otherwise it assumes git is in the path. | |
| 357 | 364 | ||
| 358 | - Note that the git executable is actually found during the refresh step in | ||
| 359 | - the top level ``__init__``. | ||
| 365 | + :note: The git executable is actually found during the refresh step in | ||
| 366 | + the top level :mod:`__init__`. It can also be changed by explicitly calling | ||
| 367 | + :func:`git.refresh`. | ||
| 360 | 368 | """ | |
| 361 | 369 | ||
| 370 | + _refresh_token = object() # Since None would match an initial _version_info_token. | ||
| 371 | + | ||
| 362 | 372 | @classmethod | |
| 363 | 373 | def refresh(cls, path: Union[None, PathLike] = None) -> bool: | |
| 364 | - """This gets called by the refresh function (see the top level __init__).""" | ||
| 374 | + """This gets called by the refresh function (see the top level __init__). | ||
| 375 | + | ||
| 376 | + :param path: Optional path to the git executable. If not absolute, it is | ||
| 377 | + resolved immediately, relative to the current directory. (See note below.) | ||
| 378 | + | ||
| 379 | + :note: The top-level :func:`git.refresh` should be preferred because it calls | ||
| 380 | + this method and may also update other state accordingly. | ||
| 381 | + | ||
| 382 | + :note: There are three different ways to specify what command refreshing causes | ||
| 383 | + to be uses for git: | ||
| 384 | + | ||
| 385 | + 1. Pass no *path* argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE`` | ||
| 386 | + environment variable. The command name ``git`` is used. It is looked up | ||
| 387 | + in a path search by the system, in each command run (roughly similar to | ||
| 388 | + how git is found when running ``git`` commands manually). This is usually | ||
| 389 | + the desired behavior. | ||
| 390 | + | ||
| 391 | + 2. Pass no *path* argument but set the ``GIT_PYTHON_GIT_EXECUTABLE`` | ||
| 392 | + environment variable. The command given as the value of that variable is | ||
| 393 | + used. This may be a simple command or an arbitrary path. It is looked up | ||
| 394 | + in each command run. Setting ``GIT_PYTHON_GIT_EXECUTABLE`` to ``git`` has | ||
| 395 | + the same effect as not setting it. | ||
| 396 | + | ||
| 397 | + 3. Pass a *path* argument. This path, if not absolute, it immediately | ||
| 398 | + resolved, relative to the current directory. This resolution occurs at | ||
| 399 | + the time of the refresh, and when git commands are run, they are run with | ||
| 400 | + that previously resolved path. If a *path* argument is passed, the | ||
| 401 | + ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted. | ||
| 402 | + | ||
| 403 | + :note: Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class | ||
| 404 | + attribute, which can be read on the :class:`Git` class or any of its | ||
| 405 | + instances to check what command is used to run git. This attribute should | ||
| 406 | + not be confused with the related ``GIT_PYTHON_GIT_EXECUTABLE`` environment | ||
| 407 | + variable. The class attribute is set no matter how refreshing is performed. | ||
| 408 | + """ | ||
| 365 | 409 | # Discern which path to refresh with. | |
| 366 | 410 | if path is not None: | |
| 367 | 411 | new_git = os.path.expanduser(path) | |
@@ -371,7 +415,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: | |||
| 371 | 415 | ||
| 372 | 416 | # Keep track of the old and new git executable path. | |
| 373 | 417 | old_git = cls.GIT_PYTHON_GIT_EXECUTABLE | |
| 418 | + old_refresh_token = cls._refresh_token | ||
| 374 | 419 | cls.GIT_PYTHON_GIT_EXECUTABLE = new_git | |
| 420 | + cls._refresh_token = object() | ||
| 375 | 421 | ||
| 376 | 422 | # Test if the new git executable path is valid. A GitCommandNotFound error is | |
| 377 | 423 | # spawned by us. A PermissionError is spawned if the git executable cannot be | |
@@ -392,14 +438,15 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: | |||
| 392 | 438 | The git executable must be specified in one of the following ways: | |
| 393 | 439 | - be included in your $PATH | |
| 394 | 440 | - be set via $%s | |
| 395 | - - explicitly set via git.refresh() | ||
| 441 | + - explicitly set via git.refresh("/full/path/to/git") | ||
| 396 | 442 | """ | |
| 397 | 443 | ) | |
| 398 | 444 | % cls._git_exec_env_var | |
| 399 | 445 | ) | |
| 400 | 446 | ||
| 401 | 447 | # Revert to whatever the old_git was. | |
| 402 | 448 | cls.GIT_PYTHON_GIT_EXECUTABLE = old_git | |
| 449 | + cls._refresh_token = old_refresh_token | ||
| 403 | 450 | ||
| 404 | 451 | if old_git is None: | |
| 405 | 452 | # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only | |
@@ -783,6 +830,10 @@ def __init__(self, working_dir: Union[None, PathLike] = None): | |||
| 783 | 830 | # Extra environment variables to pass to git commands | |
| 784 | 831 | self._environment: Dict[str, str] = {} | |
| 785 | 832 | ||
| 833 | + # Cached version slots | ||
| 834 | + self._version_info: Union[Tuple[int, ...], None] = None | ||
| 835 | + self._version_info_token: object = None | ||
| 836 | + | ||
| 786 | 837 | # Cached command slots | |
| 787 | 838 | self.cat_file_header: Union[None, TBD] = None | |
| 788 | 839 | self.cat_file_all: Union[None, TBD] = None | |
@@ -795,8 +846,8 @@ def __getattr__(self, name: str) -> Any: | |||
| 795 | 846 | Callable object that will execute call :meth:`_call_process` with | |
| 796 | 847 | your arguments. | |
| 797 | 848 | """ | |
| 798 | - if name[0] == "_": | ||
| 799 | - return LazyMixin.__getattr__(self, name) | ||
| 849 | + if name.startswith("_"): | ||
| 850 | + return super().__getattribute__(name) | ||
| 800 | 851 | return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) | |
| 801 | 852 | ||
| 802 | 853 | def set_persistent_git_options(self, **kwargs: Any) -> None: | |
@@ -811,33 +862,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None: | |||
| 811 | 862 | ||
| 812 | 863 | self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) | |
| 813 | 864 | ||
| 814 | - def _set_cache_(self, attr: str) -> None: | ||
| 815 | - if attr == "_version_info": | ||
| 816 | - # We only use the first 4 numbers, as everything else could be strings in fact (on Windows). | ||
| 817 | - process_version = self._call_process("version") # Should be as default *args and **kwargs used. | ||
| 818 | - version_numbers = process_version.split(" ")[2] | ||
| 819 | - | ||
| 820 | - self._version_info = cast( | ||
| 821 | - Tuple[int, int, int, int], | ||
| 822 | - tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()), | ||
| 823 | - ) | ||
| 824 | - else: | ||
| 825 | - super()._set_cache_(attr) | ||
| 826 | - # END handle version info | ||
| 827 | - | ||
| 828 | 865 | @property | |
| 829 | 866 | def working_dir(self) -> Union[None, PathLike]: | |
| 830 | 867 | """:return: Git directory we are working on""" | |
| 831 | 868 | return self._working_dir | |
| 832 | 869 | ||
| 833 | 870 | @property | |
| 834 | - def version_info(self) -> Tuple[int, int, int, int]: | ||
| 871 | + def version_info(self) -> Tuple[int, ...]: | ||
| 835 | 872 | """ | |
| 836 | - :return: tuple(int, int, int, int) tuple with integers representing the major, minor | ||
| 837 | - and additional version numbers as parsed from git version. | ||
| 873 | + :return: tuple with integers representing the major, minor and additional | ||
| 874 | + version numbers as parsed from git version. Up to four fields are used. | ||
| 838 | 875 | ||
| 839 | 876 | This value is generated on demand and is cached. | |
| 840 | 877 | """ | |
| 878 | + # Refreshing is global, but version_info caching is per-instance. | ||
| 879 | + refresh_token = self._refresh_token # Copy token in case of concurrent refresh. | ||
| 880 | + | ||
| 881 | + # Use the cached version if obtained after the most recent refresh. | ||
| 882 | + if self._version_info_token is refresh_token: | ||
| 883 | + assert self._version_info is not None, "Bug: corrupted token-check state" | ||
| 884 | + return self._version_info | ||
| 885 | + | ||
| 886 | + # Run "git version" and parse it. | ||
| 887 | + process_version = self._call_process("version") | ||
| 888 | + version_string = process_version.split(" ")[2] | ||
| 889 | + version_fields = version_string.split(".")[:4] | ||
| 890 | + leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields) | ||
| 891 | + self._version_info = tuple(map(int, leading_numeric_fields)) | ||
| 892 | + | ||
| 893 | + # This value will be considered valid until the next refresh. | ||
| 894 | + self._version_info_token = refresh_token | ||
| 841 | 895 | return self._version_info | |
| 842 | 896 | ||
| 843 | 897 | @overload | |
| Back | FazBrowse Home | New Git URL |
0 commit comments