39
39
log = logging .getLogger (__name__ )
40
40
41
41
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
-
49
42
@enum .unique
50
- class _WinBash (enum .Enum , metaclass = _WinBashMeta ):
43
+ class _WinBashStatus (enum .Enum ):
51
44
"""Status of bash.exe for native Windows. Affects which commit hook tests can pass.
52
45
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.
68
47
"""
69
48
70
49
INAPPLICABLE = enum .auto ()
@@ -74,13 +53,13 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
74
53
"""No command for ``bash.exe`` is found on the system."""
75
54
76
55
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)."""
78
57
79
58
WSL = enum .auto ()
80
- """Running ``bash.exe`` runs bash on a WSL system ."""
59
+ """Running ``bash.exe`` calls `` bash`` in a WSL distribution ."""
81
60
82
61
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."""
84
63
85
64
ERROR_WHILE_CHECKING = enum .auto ()
86
65
"""Could not determine the status.
@@ -92,13 +71,34 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
92
71
"""
93
72
94
73
@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
+ """
96
95
if os .name != "nt" :
97
96
return cls .INAPPLICABLE
98
97
99
98
try :
100
99
# 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.
102
102
script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
103
103
command = ["bash.exe" , "-c" , script ]
104
104
proc = subprocess .run (command , capture_output = True , check = True , text = True )
@@ -129,6 +129,9 @@ def _error(cls, error_or_process):
129
129
return cls .ERROR_WHILE_CHECKING
130
130
131
131
132
+ _win_bash_status = _WinBashStatus .check ()
133
+
134
+
132
135
def _make_hook (git_dir , name , content , make_exec = True ):
133
136
"""A helper to create a hook"""
134
137
hp = hook_path (name , git_dir )
@@ -998,7 +1001,7 @@ class Mocked:
998
1001
self .assertEqual (rel , os .path .relpath (path , root ))
999
1002
1000
1003
@pytest .mark .xfail (
1001
- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1004
+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
1002
1005
reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
1003
1006
raises = HookExecutionError ,
1004
1007
)
@@ -1009,7 +1012,7 @@ def test_pre_commit_hook_success(self, rw_repo):
1009
1012
index .commit ("This should not fail" )
1010
1013
1011
1014
@pytest .mark .xfail (
1012
- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1015
+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
1013
1016
reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
1014
1017
raises = AssertionError ,
1015
1018
)
@@ -1020,7 +1023,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
1020
1023
try :
1021
1024
index .commit ("This should fail" )
1022
1025
except HookExecutionError as err :
1023
- if _WinBash () is _WinBash .ABSENT :
1026
+ if _win_bash_status is _WinBashStatus .ABSENT :
1024
1027
self .assertIsInstance (err .status , OSError )
1025
1028
self .assertEqual (err .command , [hp ])
1026
1029
self .assertEqual (err .stdout , "" )
@@ -1036,12 +1039,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
1036
1039
raise AssertionError ("Should have caught a HookExecutionError" )
1037
1040
1038
1041
@pytest .mark .xfail (
1039
- _WinBash () in {_WinBash .ABSENT , _WinBash .WSL },
1042
+ _win_bash_status in {_WinBashStatus .ABSENT , _WinBashStatus .WSL },
1040
1043
reason = "Specifically seems to fail on WSL bash (in spite of #1399)" ,
1041
1044
raises = AssertionError ,
1042
1045
)
1043
1046
@pytest .mark .xfail (
1044
- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1047
+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
1045
1048
reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
1046
1049
raises = HookExecutionError ,
1047
1050
)
@@ -1059,7 +1062,7 @@ def test_commit_msg_hook_success(self, rw_repo):
1059
1062
self .assertEqual (new_commit .message , "{} {}" .format (commit_message , from_hook_message ))
1060
1063
1061
1064
@pytest .mark .xfail (
1062
- _WinBash () is _WinBash .WSL_NO_DISTRO ,
1065
+ _win_bash_status is _WinBashStatus .WSL_NO_DISTRO ,
1063
1066
reason = "Currently uses the bash.exe for WSL even with no WSL distro installed" ,
1064
1067
raises = AssertionError ,
1065
1068
)
@@ -1070,7 +1073,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
1070
1073
try :
1071
1074
index .commit ("This should fail" )
1072
1075
except HookExecutionError as err :
1073
- if _WinBash () is _WinBash .ABSENT :
1076
+ if _win_bash_status is _WinBashStatus .ABSENT :
1074
1077
self .assertIsInstance (err .status , OSError )
1075
1078
self .assertEqual (err .command , [hp ])
1076
1079
self .assertEqual (err .stdout , "" )
0 commit comments