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
@@ -344,6 +350,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
344
350
for, which is not possible under most circumstances.
345
351
346
352
See:
353
+
347
354
- :meth:`Git.execute` (on the ``shell`` parameter).
348
355
- https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
349
356
- https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
@@ -355,13 +362,50 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
355
362
GIT_PYTHON_GIT_EXECUTABLE = None
356
363
"""Provide the full path to the git executable. Otherwise it assumes git is in the path.
357
364
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`.
360
368
"""
361
369
370
+ _refresh_token = object () # Since None would match an initial _version_info_token.
371
+
362
372
@classmethod
363
373
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
+ """
365
409
# Discern which path to refresh with.
366
410
if path is not None :
367
411
new_git = os .path .expanduser (path )
@@ -371,7 +415,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
371
415
372
416
# Keep track of the old and new git executable path.
373
417
old_git = cls .GIT_PYTHON_GIT_EXECUTABLE
418
+ old_refresh_token = cls ._refresh_token
374
419
cls .GIT_PYTHON_GIT_EXECUTABLE = new_git
420
+ cls ._refresh_token = object ()
375
421
376
422
# Test if the new git executable path is valid. A GitCommandNotFound error is
377
423
# 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:
392
438
The git executable must be specified in one of the following ways:
393
439
- be included in your $PATH
394
440
- be set via $%s
395
- - explicitly set via git.refresh()
441
+ - explicitly set via git.refresh("/full/path/to/git" )
396
442
"""
397
443
)
398
444
% cls ._git_exec_env_var
399
445
)
400
446
401
447
# Revert to whatever the old_git was.
402
448
cls .GIT_PYTHON_GIT_EXECUTABLE = old_git
449
+ cls ._refresh_token = old_refresh_token
403
450
404
451
if old_git is None :
405
452
# 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):
783
830
# Extra environment variables to pass to git commands
784
831
self ._environment : Dict [str , str ] = {}
785
832
833
+ # Cached version slots
834
+ self ._version_info : Union [Tuple [int , ...], None ] = None
835
+ self ._version_info_token : object = None
836
+
786
837
# Cached command slots
787
838
self .cat_file_header : Union [None , TBD ] = None
788
839
self .cat_file_all : Union [None , TBD ] = None
@@ -795,8 +846,8 @@ def __getattr__(self, name: str) -> Any:
795
846
Callable object that will execute call :meth:`_call_process` with
796
847
your arguments.
797
848
"""
798
- if name [ 0 ] == "_" :
799
- return LazyMixin . __getattr__ ( self , name )
849
+ if name . startswith ( "_" ) :
850
+ return super (). __getattribute__ ( name )
800
851
return lambda * args , ** kwargs : self ._call_process (name , * args , ** kwargs )
801
852
802
853
def set_persistent_git_options (self , ** kwargs : Any ) -> None :
@@ -811,33 +862,36 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
811
862
812
863
self ._persistent_git_options = self .transform_kwargs (split_single_char_options = True , ** kwargs )
813
864
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
865
@property
829
866
def working_dir (self ) -> Union [None , PathLike ]:
830
867
""":return: Git directory we are working on"""
831
868
return self ._working_dir
832
869
833
870
@property
834
- def version_info (self ) -> Tuple [int , int , int , int ]:
871
+ def version_info (self ) -> Tuple [int , ... ]:
835
872
"""
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 .
838
875
839
876
This value is generated on demand and is cached.
840
877
"""
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
841
895
return self ._version_info
842
896
843
897
@overload
0 commit comments