1111import logging
1212import os
1313import signal
14- from subprocess import Popen , PIPE , DEVNULL , run , CalledProcessError
14+ from subprocess import Popen , PIPE , DEVNULL
1515import subprocess
1616import threading
1717from textwrap import dedent
1818from pathlib import Path
1919
20- from git .compat import defenc , force_bytes , safe_decode , is_win
20+ from git .compat import defenc , force_bytes , safe_decode
2121from git .exc import (
2222 CommandError ,
2323 GitCommandError ,
@@ -364,24 +364,27 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
364364 _bash_exec_env_var = "GIT_PYTHON_BASH_EXECUTABLE"
365365
366366 bash_exec_name = "bash"
367- """Default bash command that should work on Linux, Windows, and other systems ."""
367+ """Default bash command."""
368368
369369 GIT_PYTHON_BASH_EXECUTABLE = None
370- """Provide the full path to the bash executable. Otherwise it assumes bash is in the path.
371-
372- Note that the bash executable is actually found during the refresh step in
373- the top level ``__init__``.
370+ """
371+ Provides the path to the bash executable used for commit hooks. This is
372+ ordinarily set by `Git.refresh_bash()`. Note that the default behavior of
373+ invoking commit hooks on Windows has changed to not prefer WSL bash with
374+ the introduction of this variable. See the `Git.refresh_bash()`
375+ documentation for details on the default values and search paths.
374376 """
375377
376378 @classmethod
377- def _get_default_bash_path (cls ):
379+ def _get_default_bash_path (cls ) -> str :
378380 # Assumes that, if user is running in Windows, they probably are using
379381 # Git for Windows, which includes Git BASH and should be associated
380- # with the configured Git command set in `refresh()`. Regardless of
381- # if the Git command assumes it is installed in (root)/cmd/git.exe or
382- # (root)/bin/git.exe, the root is always up two levels from the git
383- # command. Try going up to levels from the currently configured
384- # git command, then navigate to (root)/bin/bash.exe. If this exists,
382+ # with the configured Git command set in `refresh()`.
383+ # Uses the output of `git --exec-path` for the currently configured
384+ # Git command to find its `git-core` directory. If one assumes that
385+ # the `git-core` directory is always three levels deeper than the
386+ # root directory of the Git installation, we can try going up three
387+ # levels and then navigating to (root)/bin/bash.exe. If this exists,
385388 # prefer it over the WSL version in System32, direct access to which
386389 # is reportedly deprecated. Fail back to default "bash.exe" if
387390 # the Git for Windows lookup doesn't work.
@@ -392,145 +395,73 @@ def _get_default_bash_path(cls):
392395 # independently of the Windows Git. A noteworthy example are repos
393396 # with Git LFS, where Git LFS may be installed in Windows but not
394397 # in WSL.
395- if not is_win :
398+ if os . name != 'nt' :
396399 return "bash"
397- try :
398- wheregit = run (["where" , Git .GIT_PYTHON_GIT_EXECUTABLE ], check = True , stdout = PIPE ).stdout
399- except CalledProcessError :
400- return "bash.exe"
401- gitpath = Path (wheregit .decode (defenc ).splitlines ()[0 ])
402- gitroot = gitpath .parent .parent
400+ gitcore = Path (cls ()._call_process ("--exec-path" ))
401+ gitroot = gitcore .parent .parent .parent
403402 gitbash = gitroot / "bin" / "bash.exe"
404403 return str (gitbash ) if gitbash .exists () else "bash.exe"
405404
406405 @classmethod
407406 def refresh_bash (cls , path : Union [None , PathLike ] = None ) -> bool :
408- """This gets called by the refresh function (see the top level __init__)."""
407+ """
408+ Refreshes the cached path to the bash executable used for executing
409+ commit hook scripts. This gets called by the top-level `refresh()`
410+ function on initial package import (see the top level __init__), but
411+ this method may be invoked manually if the path changes after import.
412+
413+ This method only checks one path for a valid bash executable at a time,
414+ using the first non-empty path provided in the following priority
415+ order:
416+
417+ 1. the explicit `path` argument to this method
418+ 2. the environment variable `GIT_PYTHON_BASH_EXECUTABLE` if it is set
419+ and available via `os.environ` upon calling this method
420+ 3. if the current platform is not Windows, the simple string `"bash"`
421+ 4. if the current platform is Windows, inferred from the current
422+ provided Git executable assuming it is part of a Git for Windows
423+ distribution.
424+
425+ The current platform is checked based on the call `os.name`.
426+
427+ This is a change to the default behavior from previous versions of
428+ GitPython. In the event backwards compatibility is needed, the `path`
429+ argument or the environment variable may be set to the string
430+ `"bash.exe"`, which on most systems invokes the WSL bash by default.
431+
432+ This change to default behavior addresses issues where git hooks are
433+ intended to run assuming the "native" Windows environment as seen by
434+ git.exe rather than inside the git sandbox of WSL, which is likely
435+ configured independently of the Windows Git. A noteworthy example are
436+ repos with Git LFS, where Git LFS may be installed in Windows but not
437+ in WSL.
438+ """
409439 # Discern which path to refresh with.
410440 if path is not None :
411441 new_bash = os .path .expanduser (path )
412- new_bash = os .path .abspath (new_bash )
442+ # new_bash = os.path.abspath(new_bash)
413443 else :
414444 new_bash = os .environ .get (cls ._bash_exec_env_var )
415445 if not new_bash :
416446 new_bash = cls ._get_default_bash_path ()
417447
418448 # Keep track of the old and new bash executable path.
419- old_bash = cls .GIT_PYTHON_BASH_EXECUTABLE
449+ # old_bash = cls.GIT_PYTHON_BASH_EXECUTABLE
420450 cls .GIT_PYTHON_BASH_EXECUTABLE = new_bash
421451
422- # Test if the new git executable path is valid. A GitCommandNotFound error is
423- # spawned by us. A PermissionError is spawned if the git executable cannot be
424- # executed for whatever reason.
425- has_bash = False
426- try :
427- run ([cls .GIT_PYTHON_BASH_EXECUTABLE , "--version" ], check = True , stdout = PIPE )
428- has_bash = True
429- except CalledProcessError :
430- pass
431-
432- # Warn or raise exception if test failed.
433- if not has_bash :
434- err = dedent (
435- f"""\
436- Bad bash executable.
437- The bash executable must be specified in one of the following ways:
438- - be included in your $PATH
439- - be set via ${ cls ._bash_exec_env_var }
440- - explicitly set via git.refresh_bash()
441- """
442- )
443-
444- # Revert to whatever the old_bash was.
445- cls .GIT_PYTHON_BASH_EXECUTABLE = old_bash
446-
447- if old_bash is None :
448- # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
449- # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
450-
451- # Determine what the user wants to happen during the initial refresh we
452- # expect GIT_PYTHON_REFRESH to either be unset or be one of the
453- # following values:
454- #
455- # 0|q|quiet|s|silence|n|none
456- # 1|w|warn|warning
457- # 2|r|raise|e|error
458-
459- mode = os .environ .get (cls ._refresh_env_var , "raise" ).lower ()
460-
461- quiet = ["quiet" , "q" , "silence" , "s" , "none" , "n" , "0" ]
462- warn = ["warn" , "w" , "warning" , "1" ]
463- error = ["error" , "e" , "raise" , "r" , "2" ]
464-
465- if mode in quiet :
466- pass
467- elif mode in warn or mode in error :
468- err = (
469- dedent (
470- """\
471- %s
472- All commit hook commands will error until this is rectified.
473-
474- This initial warning can be silenced or aggravated in the future by setting the
475- $%s environment variable. Use one of the following values:
476- - %s: for no warning or exception
477- - %s: for a printed warning
478- - %s: for a raised exception
479-
480- Example:
481- export %s=%s
482- """
483- )
484- % (
485- err ,
486- cls ._refresh_env_var ,
487- "|" .join (quiet ),
488- "|" .join (warn ),
489- "|" .join (error ),
490- cls ._refresh_env_var ,
491- quiet [0 ],
492- )
493- )
494-
495- if mode in warn :
496- print ("WARNING: %s" % err )
497- else :
498- raise ImportError (err )
499- else :
500- err = (
501- dedent (
502- """\
503- %s environment variable has been set but it has been set with an invalid value.
504-
505- Use only the following values:
506- - %s: for no warning or exception
507- - %s: for a printed warning
508- - %s: for a raised exception
509- """
510- )
511- % (
512- cls ._refresh_env_var ,
513- "|" .join (quiet ),
514- "|" .join (warn ),
515- "|" .join (error ),
516- )
517- )
518- raise ImportError (err )
519-
520- # We get here if this was the init refresh and the refresh mode was not
521- # error. Go ahead and set the GIT_PYTHON_BASH_EXECUTABLE such that we
522- # discern the difference between a first import and a second import.
523- cls .GIT_PYTHON_BASH_EXECUTABLE = cls .bash_exec_name
524- else :
525- # After the first refresh (when GIT_PYTHON_BASH_EXECUTABLE is no longer
526- # None) we raise an exception.
527- raise GitCommandNotFound ("bash" , err )
528-
452+ # Test if the new git executable path exists.
453+ has_bash = Path (cls .GIT_PYTHON_BASH_EXECUTABLE ).exists ()
529454 return has_bash
530455
531456 @classmethod
532457 def refresh (cls , path : Union [None , PathLike ] = None ) -> bool :
533- """This gets called by the refresh function (see the top level __init__)."""
458+ """
459+ This gets called by the refresh function (see the top level __init__).
460+
461+ Note that calling this method directly does not automatically update
462+ the cached path to `bash`; either invoke the top level `refresh()`
463+ function or call `Git.refresh_bash()` directly.
464+ """
534465 # Discern which path to refresh with.
535466 if path is not None :
536467 new_git = os .path .expanduser (path )
0 commit comments