88import re
99import contextlib
1010import io
11+ import itertools
1112import logging
1213import os
1314import signal
2526 UnsafeProtocolError ,
2627)
2728from 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