3939log = logging .getLogger (__name__ )
4040
4141
42- class _WinBashMeta (enum .EnumMeta ):
43- """Metaclass allowing :class:`_WinBash` custom behavior when called."""
44-
45- def __call__ (cls ):
46- return cls ._check ()
47-
48-
4942@enum .unique
50- class _WinBash (enum .Enum , metaclass = _WinBashMeta ):
43+ class _WinBashStatus (enum .Enum ):
5144 """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
5245
53- Call ``_WinBash()`` to check the status.
54-
55- This can't be reliably discovered using :func:`shutil.which`, as that approximates
56- how a shell is expected to search for an executable. On Windows, there are major
57- differences between how executables are found by a shell and otherwise. (This is the
58- cmd.exe Windows shell and should not be confused with bash.exe.) Our run_commit_hook
59- function in GitPython uses subprocess.Popen, including to run bash.exe on Windows.
60- It does not pass shell=True (and should not). Popen calls CreateProcessW, which
61- searches several locations prior to using the PATH environment variable. It is
62- expected to search the System32 directory, even if another directory containing the
63- executable precedes it in PATH. (There are other differences, less relevant here.)
64- When WSL is installed, even if no WSL *systems* are installed, bash.exe exists in
65- System32, and Popen finds it even if another bash.exe precedes it in PATH, as
66- happens on CI. If WSL is absent, System32 may still have bash.exe, as Windows users
67- and administrators occasionally copy executables there in lieu of extending PATH.
46+ Call :meth:`check` to check the status.
6847 """
6948
7049 INAPPLICABLE = enum .auto ()
@@ -74,13 +53,13 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
7453 """No command for ``bash.exe`` is found on the system."""
7554
7655 NATIVE = enum .auto ()
77- """Running ``bash.exe`` operates outside any WSL environment (as with Git Bash)."""
56+ """Running ``bash.exe`` operates outside any WSL distribution (as with Git Bash)."""
7857
7958 WSL = enum .auto ()
80- """Running ``bash.exe`` runs bash on a WSL system ."""
59+ """Running ``bash.exe`` calls `` bash`` in a WSL distribution ."""
8160
8261 WSL_NO_DISTRO = enum .auto ()
83- """Running ``bash.exe` tries to run bash on a WSL system , but none exists."""
62+ """Running ``bash.exe` tries to run bash on a WSL distribution , but none exists."""
8463
8564 ERROR_WHILE_CHECKING = enum .auto ()
8665 """Could not determine the status.
@@ -92,13 +71,34 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
9271 """
9372
9473 @classmethod
95- def _check (cls ):
74+ def check (cls ):
75+ """Check the status of the ``bash.exe`` :func:`index.fun.run_commit_hook` uses.
76+
77+ This uses EAFP, attempting to run a command via ``bash.exe``. Which ``bash.exe``
78+ is used can't be reliably discovered by :func:`shutil.which`, which approximates
79+ how a shell is expected to search for an executable. On Windows, there are major
80+ differences between how executables are found by a shell and otherwise. (This is
81+ the cmd.exe Windows shell, and shouldn't be confused with bash.exe itself. That
82+ the command being looked up also happens to be an interpreter is not relevant.)
83+
84+ :func:`index.fun.run_commit_hook` uses :class:`subprocess.Popen`, including when
85+ it runs ``bash.exe`` on Windows. It doesn't pass ``shell=True`` (and shouldn't).
86+ On Windows, `Popen` calls ``CreateProcessW``, which searches several locations
87+ prior to using the ``PATH`` environment variable. It is expected to search the
88+ ``System32`` directory, even if another directory containing the executable
89+ precedes it in ``PATH``. (Other differences are less relevant here.) When WSL is
90+ installed, even with no distributions, ``bash.exe`` exists in ``System32``, and
91+ `Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
92+ If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
93+ administrators occasionally put executables there in lieu of extending ``PATH``.
94+ """
9695 if os .name != "nt" :
9796 return cls .INAPPLICABLE
9897
9998 try :
10099 # Print rather than returning the test command's exit status so that if a
101- # failure occurs before we even get to this point, we will detect it.
100+ # failure occurs before we even get to this point, we will detect it. See
101+ # https://superuser.com/a/1749811 for information on ways to check for WSL.
102102 script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
103103 command = ["bash.exe" , "-c" , script ]
104104 proc = subprocess .run (command , capture_output = True , check = True , text = True )
@@ -129,6 +129,9 @@ def _error(cls, error_or_process):
129129 return cls .ERROR_WHILE_CHECKING
130130
131131
132+ _win_bash_status = _WinBashStatus .check ()
133+
134+
132135def _make_hook (git_dir , name , content , make_exec = True ):
133136 """A helper to create a hook"""
134137 hp = hook_path (name , git_dir )
@@ -998,7 +1001,7 @@ class Mocked:
9981001 self .assertEqual (rel , os .path .relpath (path , root ))
9991002
10001003 @pytest .mark .xfail (
1001- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1004+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
10021005 reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
10031006 raises = HookExecutionError ,
10041007 )
@@ -1009,7 +1012,7 @@ def test_pre_commit_hook_success(self, rw_repo):
10091012 index .commit ("This should not fail" )
10101013
10111014 @pytest .mark .xfail (
1012- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1015+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
10131016 reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
10141017 raises = AssertionError ,
10151018 )
@@ -1020,7 +1023,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
10201023 try :
10211024 index .commit ("This should fail" )
10221025 except HookExecutionError as err :
1023- if _WinBash () is _WinBash .ABSENT :
1026+ if _win_bash_status is _WinBashStatus .ABSENT :
10241027 self .assertIsInstance (err .status , OSError )
10251028 self .assertEqual (err .command , [hp ])
10261029 self .assertEqual (err .stdout , "" )
@@ -1036,12 +1039,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
10361039 raise AssertionError ("Should have caught a HookExecutionError" )
10371040
10381041 @pytest .mark .xfail (
1039- _WinBash () in {_WinBash .ABSENT , _WinBash .WSL },
1042+ _win_bash_status in {_WinBashStatus .ABSENT , _WinBashStatus .WSL },
10401043 reason = "Specifically seems to fail on WSL bash (in spite of #1399)" ,
10411044 raises = AssertionError ,
10421045 )
10431046 @pytest .mark .xfail (
1044- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1047+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
10451048 reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
10461049 raises = HookExecutionError ,
10471050 )
@@ -1059,7 +1062,7 @@ def test_commit_msg_hook_success(self, rw_repo):
10591062 self .assertEqual (new_commit .message , "{} {}" .format (commit_message , from_hook_message ))
10601063
10611064 @pytest .mark .xfail (
1062- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1065+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
10631066 reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
10641067 raises = AssertionError ,
10651068 )
@@ -1070,7 +1073,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
10701073 try :
10711074 index .commit ("This should fail" )
10721075 except HookExecutionError as err :
1073- if _WinBash () is _WinBash .ABSENT :
1076+ if _win_bash_status is _WinBashStatus .ABSENT :
10741077 self .assertIsInstance (err .status , OSError )
10751078 self .assertEqual (err .command , [hp ])
10761079 self .assertEqual (err .stdout , "" )
0 commit comments