11
11
import logging
12
12
import os
13
13
import signal
14
- from subprocess import Popen , PIPE , DEVNULL , run , CalledProcessError
14
+ from subprocess import Popen , PIPE , DEVNULL
15
15
import subprocess
16
16
import threading
17
17
from textwrap import dedent
18
18
from pathlib import Path
19
19
20
- from git .compat import defenc , force_bytes , safe_decode , is_win
20
+ from git .compat import defenc , force_bytes , safe_decode
21
21
from git .exc import (
22
22
CommandError ,
23
23
GitCommandError ,
@@ -364,24 +364,27 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
364
364
_bash_exec_env_var = "GIT_PYTHON_BASH_EXECUTABLE"
365
365
366
366
bash_exec_name = "bash"
367
- """Default bash command that should work on Linux, Windows, and other systems ."""
367
+ """Default bash command."""
368
368
369
369
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.
374
376
"""
375
377
376
378
@classmethod
377
- def _get_default_bash_path (cls ):
379
+ def _get_default_bash_path (cls ) -> str :
378
380
# Assumes that, if user is running in Windows, they probably are using
379
381
# 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,
385
388
# prefer it over the WSL version in System32, direct access to which
386
389
# is reportedly deprecated. Fail back to default "bash.exe" if
387
390
# the Git for Windows lookup doesn't work.
@@ -392,145 +395,73 @@ def _get_default_bash_path(cls):
392
395
# independently of the Windows Git. A noteworthy example are repos
393
396
# with Git LFS, where Git LFS may be installed in Windows but not
394
397
# in WSL.
395
- if not is_win :
398
+ if os . name != 'nt' :
396
399
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
403
402
gitbash = gitroot / "bin" / "bash.exe"
404
403
return str (gitbash ) if gitbash .exists () else "bash.exe"
405
404
406
405
@classmethod
407
406
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
+ """
409
439
# Discern which path to refresh with.
410
440
if path is not None :
411
441
new_bash = os .path .expanduser (path )
412
- new_bash = os .path .abspath (new_bash )
442
+ # new_bash = os.path.abspath(new_bash)
413
443
else :
414
444
new_bash = os .environ .get (cls ._bash_exec_env_var )
415
445
if not new_bash :
416
446
new_bash = cls ._get_default_bash_path ()
417
447
418
448
# 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
420
450
cls .GIT_PYTHON_BASH_EXECUTABLE = new_bash
421
451
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 ()
529
454
return has_bash
530
455
531
456
@classmethod
532
457
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
+ """
534
465
# Discern which path to refresh with.
535
466
if path is not None :
536
467
new_git = os .path .expanduser (path )
0 commit comments