Skip to content

Commit 77f74ce

Browse files
committed
initial patch to use git bash rather than WSL
1 parent 32c02d1 commit 77f74ce

File tree

3 files changed

+175
-4
lines changed

3 files changed

+175
-4
lines changed

Diff for: git/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def refresh(path: Optional[PathLike] = None) -> None:
126126

127127
if not Git.refresh(path=path):
128128
return
129+
Git.refresh_bash()
129130
if not FetchInfo.refresh(): # noqa: F405
130131
return # type: ignore [unreachable]
131132

Diff for: git/cmd.py

+172-2
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
import logging
1212
import os
1313
import signal
14-
from subprocess import Popen, PIPE, DEVNULL
14+
from subprocess import Popen, PIPE, DEVNULL, run, CalledProcessError
1515
import subprocess
1616
import threading
1717
from textwrap import dedent
18+
from pathlib import Path
1819

19-
from git.compat import defenc, force_bytes, safe_decode
20+
from git.compat import defenc, force_bytes, safe_decode, is_win
2021
from git.exc import (
2122
CommandError,
2223
GitCommandError,
@@ -305,6 +306,175 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
305306
the top level ``__init__``.
306307
"""
307308

309+
_bash_exec_env_var = "GIT_PYTHON_BASH_EXECUTABLE"
310+
311+
bash_exec_name = "bash"
312+
"""Default bash command that should work on Linux, Windows, and other systems."""
313+
314+
GIT_PYTHON_BASH_EXECUTABLE = None
315+
"""Provide the full path to the bash executable. Otherwise it assumes bash is in the path.
316+
317+
Note that the bash executable is actually found during the refresh step in
318+
the top level ``__init__``.
319+
"""
320+
321+
@classmethod
322+
def _get_default_bash_path(cls):
323+
# Assumes that, if user is running in Windows, they probably are using
324+
# Git for Windows, which includes Git BASH and should be associated
325+
# with the configured Git command set in `refresh()`. Regardless of
326+
# if the Git command assumes it is installed in (root)/cmd/git.exe or
327+
# (root)/bin/git.exe, the root is always up two levels from the git
328+
# command. Try going up to levels from the currently configured
329+
# git command, then navigate to (root)/bin/bash.exe. If this exists,
330+
# prefer it over the WSL version in System32, direct access to which
331+
# is reportedly deprecated. Fail back to default "bash.exe" if
332+
# the Git for Windows lookup doesn't work.
333+
#
334+
# This addresses issues where git hooks are intended to run assuming
335+
# the "native" Windows environment as seen by git.exe rather than
336+
# inside the git sandbox of WSL, which is likely configured
337+
# independetly of the Windows Git. A noteworthy example are repos with
338+
# Git LFS, where Git LFS may be installed in Windows but not in WSL.
339+
if not is_win:
340+
return 'bash'
341+
try:
342+
wheregit = run(['where', Git.GIT_PYTHON_GIT_EXECUTABLE],
343+
check=True, stdout=PIPE).stdout
344+
except CalledProcessError:
345+
return 'bash.exe'
346+
gitpath = Path(wheregit.decode(defenc).splitlines()[0])
347+
gitroot = gitpath.parent.parent
348+
gitbash = gitroot / 'bin' / 'bash.exe'
349+
return str(gitbash) if gitbash.exists else 'bash.exe'
350+
351+
@classmethod
352+
def refresh_bash(cls, path: Union[None, PathLike] = None) -> bool:
353+
"""This gets called by the refresh function (see the top level __init__)."""
354+
# Discern which path to refresh with.
355+
if path is not None:
356+
new_bash = os.path.expanduser(path)
357+
new_bash = os.path.abspath(new_bash)
358+
else:
359+
new_bash = os.environ.get(cls._bash_exec_env_var)
360+
if new_bash is None:
361+
new_bash = cls._get_default_bash_path()
362+
363+
# Keep track of the old and new bash executable path.
364+
old_bash = cls.GIT_PYTHON_BASH_EXECUTABLE
365+
cls.GIT_PYTHON_BASH_EXECUTABLE = new_bash
366+
367+
# Test if the new git executable path is valid. A GitCommandNotFound error is
368+
# spawned by us. A PermissionError is spawned if the git executable cannot be
369+
# executed for whatever reason.
370+
has_bash = False
371+
try:
372+
run([cls.GIT_PYTHON_BASH_EXECUTABLE, '--version'])
373+
has_bash = True
374+
except CalledProcessError:
375+
pass
376+
377+
# Warn or raise exception if test failed.
378+
if not has_bash:
379+
err = (
380+
dedent(
381+
f"""\
382+
Bad bash executable.
383+
The bash executable must be specified in one of the following ways:
384+
- be included in your $PATH
385+
- be set via ${cls._bash_exec_env_var}
386+
- explicitly set via git.refresh_bash()
387+
"""
388+
)
389+
)
390+
391+
# Revert to whatever the old_bash was.
392+
cls.GIT_PYTHON_BASH_EXECUTABLE = old_bash
393+
394+
if old_bash is None:
395+
# On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
396+
# are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
397+
398+
# Determine what the user wants to happen during the initial refresh we
399+
# expect GIT_PYTHON_REFRESH to either be unset or be one of the
400+
# following values:
401+
#
402+
# 0|q|quiet|s|silence|n|none
403+
# 1|w|warn|warning
404+
# 2|r|raise|e|error
405+
406+
mode = os.environ.get(cls._refresh_env_var, "raise").lower()
407+
408+
quiet = ["quiet", "q", "silence", "s", "none", "n", "0"]
409+
warn = ["warn", "w", "warning", "1"]
410+
error = ["error", "e", "raise", "r", "2"]
411+
412+
if mode in quiet:
413+
pass
414+
elif mode in warn or mode in error:
415+
err = (
416+
dedent(
417+
"""\
418+
%s
419+
All commit hook commands will error until this is rectified.
420+
421+
This initial warning can be silenced or aggravated in the future by setting the
422+
$%s environment variable. Use one of the following values:
423+
- %s: for no warning or exception
424+
- %s: for a printed warning
425+
- %s: for a raised exception
426+
427+
Example:
428+
export %s=%s
429+
"""
430+
)
431+
% (
432+
err,
433+
cls._refresh_env_var,
434+
"|".join(quiet),
435+
"|".join(warn),
436+
"|".join(error),
437+
cls._refresh_env_var,
438+
quiet[0],
439+
)
440+
)
441+
442+
if mode in warn:
443+
print("WARNING: %s" % err)
444+
else:
445+
raise ImportError(err)
446+
else:
447+
err = (
448+
dedent(
449+
"""\
450+
%s environment variable has been set but it has been set with an invalid value.
451+
452+
Use only the following values:
453+
- %s: for no warning or exception
454+
- %s: for a printed warning
455+
- %s: for a raised exception
456+
"""
457+
)
458+
% (
459+
cls._refresh_env_var,
460+
"|".join(quiet),
461+
"|".join(warn),
462+
"|".join(error),
463+
)
464+
)
465+
raise ImportError(err)
466+
467+
# We get here if this was the init refresh and the refresh mode was not
468+
# error. Go ahead and set the GIT_PYTHON_BASH_EXECUTABLE such that we
469+
# discern the difference between a first import and a second import.
470+
cls.GIT_PYTHON_BASH_EXECUTABLE = cls.bash_exec_name
471+
else:
472+
# After the first refresh (when GIT_PYTHON_BASH_EXECUTABLE is no longer
473+
# None) we raise an exception.
474+
raise GitCommandNotFound("bash", err)
475+
476+
return has_bash
477+
308478
@classmethod
309479
def refresh(cls, path: Union[None, PathLike] = None) -> bool:
310480
"""This gets called by the refresh function (see the top level __init__)."""

Diff for: git/index/fun.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
import subprocess
2020

21-
from git.cmd import PROC_CREATIONFLAGS, handle_process_output
21+
from git.cmd import PROC_CREATIONFLAGS, handle_process_output, Git
2222
from git.compat import defenc, force_bytes, force_text, safe_decode
2323
from git.exc import HookExecutionError, UnmergedEntriesError
2424
from git.objects.fun import (
@@ -96,7 +96,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
9696
# Windows only uses extensions to determine how to open files
9797
# (doesn't understand shebangs). Try using bash to run the hook.
9898
relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
99-
cmd = ["bash.exe", relative_hp]
99+
cmd = [Git.GIT_PYTHON_BASH_EXECUTABLE, relative_hp]
100100

101101
process = subprocess.Popen(
102102
cmd + list(args),

0 commit comments

Comments
 (0)