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
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
@@ -359,6 +365,8 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
359
365
the top level ``__init__``.
360
366
"""
361
367
368
+ _refresh_token = object () # Since None would match an initial _version_info_token.
369
+
362
370
@classmethod
363
371
def refresh (cls , path : Union [None , PathLike ] = None ) -> bool :
364
372
"""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:
371
379
372
380
# Keep track of the old and new git executable path.
373
381
old_git = cls .GIT_PYTHON_GIT_EXECUTABLE
382
+ old_refresh_token = cls ._refresh_token
374
383
cls .GIT_PYTHON_GIT_EXECUTABLE = new_git
384
+ cls ._refresh_token = object ()
375
385
376
386
# Test if the new git executable path is valid. A GitCommandNotFound error is
377
387
# 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:
400
410
401
411
# Revert to whatever the old_git was.
402
412
cls .GIT_PYTHON_GIT_EXECUTABLE = old_git
413
+ cls ._refresh_token = old_refresh_token
403
414
404
415
if old_git is None :
405
416
# 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):
783
794
# Extra environment variables to pass to git commands
784
795
self ._environment : Dict [str , str ] = {}
785
796
797
+ # Cached version slots
798
+ self ._version_info : Union [Tuple [int , ...], None ] = None
799
+ self ._version_info_token : object = None
800
+
786
801
# Cached command slots
787
802
self .cat_file_header : Union [None , TBD ] = None
788
803
self .cat_file_all : Union [None , TBD ] = None
@@ -795,8 +810,8 @@ def __getattr__(self, name: str) -> Any:
795
810
Callable object that will execute call :meth:`_call_process` with
796
811
your arguments.
797
812
"""
798
- if name [ 0 ] == "_" :
799
- return LazyMixin . __getattr__ ( self , name )
813
+ if name . startswith ( "_" ) :
814
+ return super (). __getattribute__ ( name )
800
815
return lambda * args , ** kwargs : self ._call_process (name , * args , ** kwargs )
801
816
802
817
def set_persistent_git_options (self , ** kwargs : Any ) -> None :
@@ -811,33 +826,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
811
826
812
827
self ._persistent_git_options = self .transform_kwargs (split_single_char_options = True , ** kwargs )
813
828
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
829
@property
829
830
def working_dir (self ) -> Union [None , PathLike ]:
830
831
""":return: Git directory we are working on"""
831
832
return self ._working_dir
832
833
833
834
@property
834
- def version_info (self ) -> Tuple [int , int , int , int ]:
835
+ def version_info (self ) -> Tuple [int , ... ]:
835
836
"""
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 .
838
839
839
840
This value is generated on demand and is cached.
840
841
"""
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
841
859
return self ._version_info
842
860
843
861
@overload
0 commit comments