Skip to content

Commit bbabcdc

Browse files
committed
fix: branch now up to date with upstream
2 parents f2e35e7 + 1e044ea commit bbabcdc

File tree

6 files changed

+312
-49
lines changed

6 files changed

+312
-49
lines changed

Diff for: .readthedocs.yaml

+12-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Required
55
version: 2
66

7-
# Set the OS, Python version and other tools you might need
7+
# Set the OS, Python version and other tools you might need.
88
build:
99
os: ubuntu-22.04
1010
tools:
@@ -14,22 +14,24 @@ build:
1414
# rust: "1.70"
1515
# golang: "1.20"
1616

17-
# Build documentation in the "docs/" directory with Sphinx
17+
# Build documentation in the "doc/" directory with Sphinx.
1818
sphinx:
1919
configuration: doc/source/conf.py
2020
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
2121
# builder: "dirhtml"
2222
# Fail on all warnings to avoid broken references
23-
# fail_on_warning: true
23+
fail_on_warning: true
2424

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.
2626
# formats:
27-
# - pdf
28-
# - epub
27+
# - pdf
28+
# - epub
2929

3030
# Optional but recommended, declare the Python requirements required
31-
# to build your documentation
31+
# to build your documentation.
3232
# 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

Diff for: doc/source/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
# Options for HTML output
9494
# -----------------------
9595

96-
# html_theme = "sphinx_rtd_theme"
96+
html_theme = "sphinx_rtd_theme"
9797
html_theme_options = {}
9898

9999
# The name for this set of Sphinx documents. If None, it defaults to

Diff for: git/__init__.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,21 @@
120120

121121

122122
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+
"""
124138
global GIT_OK
125139
GIT_OK = False
126140

Diff for: git/cmd.py

+80-26
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
import contextlib
1010
import io
11+
import itertools
1112
import logging
1213
import os
1314
import signal
@@ -25,7 +26,6 @@
2526
UnsafeProtocolError,
2627
)
2728
from git.util import (
28-
LazyMixin,
2929
cygpath,
3030
expand_path,
3131
is_cygwin_git,
@@ -287,7 +287,7 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
287287
## -- End Utilities -- @}
288288

289289

290-
class Git(LazyMixin):
290+
class Git:
291291
"""The Git class manages communication with the Git binary.
292292
293293
It provides a convenient interface to calling the Git binary, such as in::
@@ -307,12 +307,18 @@ class Git(LazyMixin):
307307
"cat_file_all",
308308
"cat_file_header",
309309
"_version_info",
310+
"_version_info_token",
310311
"_git_options",
311312
"_persistent_git_options",
312313
"_environment",
313314
)
314315

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+
)
316322

317323
re_unsafe_protocol = re.compile(r"(.+)::.+")
318324

@@ -344,6 +350,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
344350
for, which is not possible under most circumstances.
345351
346352
See:
353+
347354
- :meth:`Git.execute` (on the ``shell`` parameter).
348355
- https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
349356
- https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
@@ -355,13 +362,50 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
355362
GIT_PYTHON_GIT_EXECUTABLE = None
356363
"""Provide the full path to the git executable. Otherwise it assumes git is in the path.
357364
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`.
360368
"""
361369

370+
_refresh_token = object() # Since None would match an initial _version_info_token.
371+
362372
@classmethod
363373
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+
"""
365409
# Discern which path to refresh with.
366410
if path is not None:
367411
new_git = os.path.expanduser(path)
@@ -371,7 +415,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
371415

372416
# Keep track of the old and new git executable path.
373417
old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
418+
old_refresh_token = cls._refresh_token
374419
cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
420+
cls._refresh_token = object()
375421

376422
# Test if the new git executable path is valid. A GitCommandNotFound error is
377423
# 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:
392438
The git executable must be specified in one of the following ways:
393439
- be included in your $PATH
394440
- be set via $%s
395-
- explicitly set via git.refresh()
441+
- explicitly set via git.refresh("/full/path/to/git")
396442
"""
397443
)
398444
% cls._git_exec_env_var
399445
)
400446

401447
# Revert to whatever the old_git was.
402448
cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
449+
cls._refresh_token = old_refresh_token
403450

404451
if old_git is None:
405452
# 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):
783830
# Extra environment variables to pass to git commands
784831
self._environment: Dict[str, str] = {}
785832

833+
# Cached version slots
834+
self._version_info: Union[Tuple[int, ...], None] = None
835+
self._version_info_token: object = None
836+
786837
# Cached command slots
787838
self.cat_file_header: Union[None, TBD] = None
788839
self.cat_file_all: Union[None, TBD] = None
@@ -795,8 +846,8 @@ def __getattr__(self, name: str) -> Any:
795846
Callable object that will execute call :meth:`_call_process` with
796847
your arguments.
797848
"""
798-
if name[0] == "_":
799-
return LazyMixin.__getattr__(self, name)
849+
if name.startswith("_"):
850+
return super().__getattribute__(name)
800851
return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
801852

802853
def set_persistent_git_options(self, **kwargs: Any) -> None:
@@ -811,33 +862,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
811862

812863
self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
813864

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-
828865
@property
829866
def working_dir(self) -> Union[None, PathLike]:
830867
""":return: Git directory we are working on"""
831868
return self._working_dir
832869

833870
@property
834-
def version_info(self) -> Tuple[int, int, int, int]:
871+
def version_info(self) -> Tuple[int, ...]:
835872
"""
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.
838875
839876
This value is generated on demand and is cached.
840877
"""
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
841895
return self._version_info
842896

843897
@overload

0 commit comments

Comments
 (0)