Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f065d1f

Browse files
committedJan 21, 2024
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
 

‎.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/setup-wsl@v2.0.2
41-
with:
42-
distribution: Debian
38+
# - name: Set up WSL (Windows)
39+
# if: startsWith(matrix.os, 'windows')
40+
# uses: Vampire/setup-wsl@v2.0.2
41+
# with:
42+
# distribution: Debian
4343

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

‎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

‎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)

‎test/test_index.py

+45-45
Original file line numberDiff line numberDiff line change
@@ -1019,16 +1019,16 @@ class Mocked:
10191019
rel = index._to_relative_path(path)
10201020
self.assertEqual(rel, os.path.relpath(path, root))
10211021

1022-
@pytest.mark.xfail(
1023-
type(_win_bash_status) is WinBashStatus.Absent,
1024-
reason="Can't run a hook on Windows without bash.exe.",
1025-
rasies=HookExecutionError,
1026-
)
1027-
@pytest.mark.xfail(
1028-
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1029-
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1030-
raises=HookExecutionError,
1031-
)
1022+
# @pytest.mark.xfail(
1023+
# type(_win_bash_status) is WinBashStatus.Absent,
1024+
# reason="Can't run a hook on Windows without bash.exe.",
1025+
# rasies=HookExecutionError,
1026+
# )
1027+
# @pytest.mark.xfail(
1028+
# type(_win_bash_status) is WinBashStatus.WslNoDistro,
1029+
# reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1030+
# raises=HookExecutionError,
1031+
# )
10321032
@with_rw_repo("HEAD", bare=True)
10331033
def test_run_commit_hook(self, rw_repo):
10341034
index = rw_repo.index
@@ -1078,27 +1078,27 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
10781078
output = Path(rw_dir, "output.txt").read_text(encoding="utf-8")
10791079
self.assertEqual(output, "Ran intended hook.\n")
10801080

1081-
@pytest.mark.xfail(
1082-
type(_win_bash_status) is WinBashStatus.Absent,
1083-
reason="Can't run a hook on Windows without bash.exe.",
1084-
rasies=HookExecutionError,
1085-
)
1086-
@pytest.mark.xfail(
1087-
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1088-
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1089-
raises=HookExecutionError,
1090-
)
1081+
# @pytest.mark.xfail(
1082+
# type(_win_bash_status) is WinBashStatus.Absent,
1083+
# reason="Can't run a hook on Windows without bash.exe.",
1084+
# rasies=HookExecutionError,
1085+
# )
1086+
# @pytest.mark.xfail(
1087+
# type(_win_bash_status) is WinBashStatus.WslNoDistro,
1088+
# reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1089+
# raises=HookExecutionError,
1090+
# )
10911091
@with_rw_repo("HEAD", bare=True)
10921092
def test_pre_commit_hook_success(self, rw_repo):
10931093
index = rw_repo.index
10941094
_make_hook(index.repo.git_dir, "pre-commit", "exit 0")
10951095
index.commit("This should not fail")
10961096

1097-
@pytest.mark.xfail(
1098-
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1099-
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1100-
raises=AssertionError,
1101-
)
1097+
# @pytest.mark.xfail(
1098+
# type(_win_bash_status) is WinBashStatus.WslNoDistro,
1099+
# reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1100+
# raises=AssertionError,
1101+
# )
11021102
@with_rw_repo("HEAD", bare=True)
11031103
def test_pre_commit_hook_fail(self, rw_repo):
11041104
index = rw_repo.index
@@ -1121,21 +1121,21 @@ def test_pre_commit_hook_fail(self, rw_repo):
11211121
else:
11221122
raise AssertionError("Should have caught a HookExecutionError")
11231123

1124-
@pytest.mark.xfail(
1125-
type(_win_bash_status) is WinBashStatus.Absent,
1126-
reason="Can't run a hook on Windows without bash.exe.",
1127-
rasies=HookExecutionError,
1128-
)
1129-
@pytest.mark.xfail(
1130-
type(_win_bash_status) is WinBashStatus.Wsl,
1131-
reason="Specifically seems to fail on WSL bash (in spite of #1399)",
1132-
raises=AssertionError,
1133-
)
1134-
@pytest.mark.xfail(
1135-
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1136-
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1137-
raises=HookExecutionError,
1138-
)
1124+
# @pytest.mark.xfail(
1125+
# type(_win_bash_status) is WinBashStatus.Absent,
1126+
# reason="Can't run a hook on Windows without bash.exe.",
1127+
# rasies=HookExecutionError,
1128+
# )
1129+
# @pytest.mark.xfail(
1130+
# type(_win_bash_status) is WinBashStatus.Wsl,
1131+
# reason="Specifically seems to fail on WSL bash (in spite of #1399)",
1132+
# raises=AssertionError,
1133+
# )
1134+
# @pytest.mark.xfail(
1135+
# type(_win_bash_status) is WinBashStatus.WslNoDistro,
1136+
# reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1137+
# raises=HookExecutionError,
1138+
# )
11391139
@with_rw_repo("HEAD", bare=True)
11401140
def test_commit_msg_hook_success(self, rw_repo):
11411141
commit_message = "commit default head by Frèderic Çaufl€"
@@ -1149,11 +1149,11 @@ def test_commit_msg_hook_success(self, rw_repo):
11491149
new_commit = index.commit(commit_message)
11501150
self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
11511151

1152-
@pytest.mark.xfail(
1153-
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1154-
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1155-
raises=AssertionError,
1156-
)
1152+
# @pytest.mark.xfail(
1153+
# type(_win_bash_status) is WinBashStatus.WslNoDistro,
1154+
# reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1155+
# raises=AssertionError,
1156+
# )
11571157
@with_rw_repo("HEAD", bare=True)
11581158
def test_commit_msg_hook_fail(self, rw_repo):
11591159
index = rw_repo.index

0 commit comments

Comments
 (0)
Please sign in to comment.