Skip to content

Commit f065d1f

Browse files
committed
address first round of comments from PR #1791
1 parent bf919f6 commit f065d1f

File tree

4 files changed

+120
-182
lines changed

4 files changed

+120
-182
lines changed

Diff for: .github/workflows/pythonpackage.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ jobs:
3535
python-version: ${{ matrix.python-version }}
3636
allow-prereleases: ${{ matrix.experimental }}
3737

38-
- name: Set up WSL (Windows)
39-
if: startsWith(matrix.os, 'windows')
40-
uses: Vampire/[email protected]
41-
with:
42-
distribution: Debian
38+
# - name: Set up WSL (Windows)
39+
# if: startsWith(matrix.os, 'windows')
40+
# uses: Vampire/[email protected]
41+
# with:
42+
# distribution: Debian
4343

4444
- name: Prepare this repo for tests
4545
run: |

Diff for: git/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,14 @@
120120

121121

122122
def refresh(path: Optional[PathLike] = None) -> None:
123-
"""Convenience method for setting the git executable path."""
123+
"""
124+
Convenience method for setting the git and bash executable paths.
125+
126+
Note that the default behavior of invoking commit hooks on Windows has
127+
changed to not prefer WSL bash with the introduction of
128+
`Git.GIT_PYTHON_BASH_EXECUTABLE`. See the `refresh_bash()` documentation
129+
for details on the default values and search paths.
130+
"""
124131
global GIT_OK
125132
GIT_OK = False
126133

Diff for: git/cmd.py

+62-131
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
import logging
1212
import os
1313
import signal
14-
from subprocess import Popen, PIPE, DEVNULL, run, CalledProcessError
14+
from subprocess import Popen, PIPE, DEVNULL
1515
import subprocess
1616
import threading
1717
from textwrap import dedent
1818
from 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
2121
from 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

Comments
 (0)