Skip to content

Commit eba6fce

Browse files
authored
Merge pull request #1838 from EliahKagan/refresh-version
Fix version_info cache invalidation, typing, parsing, and serialization
2 parents afa5754 + 629fd87 commit eba6fce

File tree

3 files changed

+244
-33
lines changed

3 files changed

+244
-33
lines changed

git/cmd.py

Lines changed: 40 additions & 22 deletions
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

@@ -359,6 +365,8 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
359365
the top level ``__init__``.
360366
"""
361367

368+
_refresh_token = object() # Since None would match an initial _version_info_token.
369+
362370
@classmethod
363371
def refresh(cls, path: Union[None, PathLike] = None) -> bool:
364372
"""This gets called by the refresh function (see the top level __init__)."""
@@ -371,7 +379,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
371379

372380
# Keep track of the old and new git executable path.
373381
old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
382+
old_refresh_token = cls._refresh_token
374383
cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
384+
cls._refresh_token = object()
375385

376386
# Test if the new git executable path is valid. A GitCommandNotFound error is
377387
# spawned by us. A PermissionError is spawned if the git executable cannot be
@@ -400,6 +410,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
400410

401411
# Revert to whatever the old_git was.
402412
cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
413+
cls._refresh_token = old_refresh_token
403414

404415
if old_git is None:
405416
# On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
@@ -783,6 +794,10 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
783794
# Extra environment variables to pass to git commands
784795
self._environment: Dict[str, str] = {}
785796

797+
# Cached version slots
798+
self._version_info: Union[Tuple[int, ...], None] = None
799+
self._version_info_token: object = None
800+
786801
# Cached command slots
787802
self.cat_file_header: Union[None, TBD] = None
788803
self.cat_file_all: Union[None, TBD] = None
@@ -795,8 +810,8 @@ def __getattr__(self, name: str) -> Any:
795810
Callable object that will execute call :meth:`_call_process` with
796811
your arguments.
797812
"""
798-
if name[0] == "_":
799-
return LazyMixin.__getattr__(self, name)
813+
if name.startswith("_"):
814+
return super().__getattribute__(name)
800815
return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
801816

802817
def set_persistent_git_options(self, **kwargs: Any) -> None:
@@ -811,33 +826,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
811826

812827
self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
813828

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-
828829
@property
829830
def working_dir(self) -> Union[None, PathLike]:
830831
""":return: Git directory we are working on"""
831832
return self._working_dir
832833

833834
@property
834-
def version_info(self) -> Tuple[int, int, int, int]:
835+
def version_info(self) -> Tuple[int, ...]:
835836
"""
836-
:return: tuple(int, int, int, int) tuple with integers representing the major, minor
837-
and additional version numbers as parsed from git version.
837+
:return: tuple with integers representing the major, minor and additional
838+
version numbers as parsed from git version. Up to four fields are used.
838839
839840
This value is generated on demand and is cached.
840841
"""
842+
# Refreshing is global, but version_info caching is per-instance.
843+
refresh_token = self._refresh_token # Copy token in case of concurrent refresh.
844+
845+
# Use the cached version if obtained after the most recent refresh.
846+
if self._version_info_token is refresh_token:
847+
assert self._version_info is not None, "Bug: corrupted token-check state"
848+
return self._version_info
849+
850+
# Run "git version" and parse it.
851+
process_version = self._call_process("version")
852+
version_string = process_version.split(" ")[2]
853+
version_fields = version_string.split(".")[:4]
854+
leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields)
855+
self._version_info = tuple(map(int, leading_numeric_fields))
856+
857+
# This value will be considered valid until the next refresh.
858+
self._version_info_token = refresh_token
841859
return self._version_info
842860

843861
@overload

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy