88import re
99import contextlib
1010import io
11+ import itertools
1112import logging
1213import os
1314import signal
2526UnsafeProtocolError ,
2627)
2728from git .util import (
28- LazyMixin ,
2929cygpath ,
3030expand_path ,
3131is_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
317323re_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
363371def 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.
373381old_git = cls .GIT_PYTHON_GIT_EXECUTABLE
382+ old_refresh_token = cls ._refresh_token
374383cls .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.
402412cls .GIT_PYTHON_GIT_EXECUTABLE = old_git
413+ cls ._refresh_token = old_refresh_token
403414
404415if 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
784795self ._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
787802self .cat_file_header :Union [None ,TBD ]= None
788803self .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 )
800815return lambda * args ,** kwargs :self ._call_process (name ,* args ,** kwargs )
801816
802817def set_persistent_git_options (self ,** kwargs :Any )-> None :
@@ -811,33 +826,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
811826
812827self ._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
829830def working_dir (self )-> Union [None ,PathLike ]:
830831""":return: Git directory we are working on"""
831832return 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
841859return self ._version_info
842860
843861@overload