Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork938
Description
The bug
Even when the Git command thatGit.refresh
runs and checks is notgit
, its command line is reported asgit
in theGitCommandNotFound
exception, when such an exception is raised:
Lines 483 to 485 ind28c20b
# After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer | |
# None) we raise an exception. | |
raiseGitCommandNotFound("git",err) |
Steps to reproduce
The effect happens when the exception is raised due to a subsequent call toGit.refresh
, since that is whenGit.refresh
raisesGitCommandNotFound
rather thanImportError
. Thecommand
attribute is directly part of the traceback message, presented ascmdline
. This can be produced by running a shell one-liner:
$ GIT_PYTHON_GIT_EXECUTABLE=git-2.43 GIT_PYTHON_REFRESH=quiet python -c 'import git; git.refresh()'Traceback (most recent call last): File "<string>", line 1, in <module> File "/home/ek/repos-wsl/GitPython/git/__init__.py", line 127, in refresh if not Git.refresh(path=path): ^^^^^^^^^^^^^^^^^^^^^^ File "/home/ek/repos-wsl/GitPython/git/cmd.py", line 485, in refresh raise GitCommandNotFound("git", err)git.exc.GitCommandNotFound: Cmd('git') not found due to: 'Bad git executable.The git executable must be specified in one of the following ways: - be included in your $PATH - be set via $GIT_PYTHON_GIT_EXECUTABLE - explicitly set via git.refresh()' cmdline: git
Although often a custom Git command may be a full path whose last component is exactlygit
(or sometimesgit.exe
on Windows), the above shows a realistic scenario in which even that may not be the case. I do not have a command namedgit-2.43
installed, so it is correct that I get an error, but the error should not state the name of the command asgit
.
Verification
Verbose debugging confirms that the command given inGIT_PYTHON_GIT_EXECUTABLE
really is running, and that the only problem is that the hard-coded command namegit
is printed instead:
$ GIT_PYTHON_GIT_EXECUTABLE=git-2.43 GIT_PYTHON_REFRESH=quiet GIT_PYTHON_TRACE=full python -c 'import logging; logging.basicConfig(level=logging.DEBUG); import git; git.refresh()'DEBUG:git.cmd:Popen(['git-2.43', 'version'], cwd=/home/ek/repos-wsl/GitPython, stdin=None, shell=False, universal_newlines=False)DEBUG:git.cmd:Popen(['git-2.43', 'version'], cwd=/home/ek/repos-wsl/GitPython, stdin=None, shell=False, universal_newlines=False)Traceback (most recent call last): ... cmdline: git
Capturing thesubprocess.Popen
audit event verifies that no other logging bug in GitPython is confusing the analysis:
$ GIT_PYTHON_GIT_EXECUTABLE=git-2.43 GIT_PYTHON_REFRESH=quiet python -c 'import sys, git; sys.addaudithook(lambda event, args: event == "subprocess.Popen" and print(args[:2])); git.refresh()'('git-2.43', ['git-2.43', 'version'])Traceback (most recent call last): ... cmdline: git
(For both the above runs, I replaced most of the traceback with...
.)
Why this is a bug
Since it's possible, however unlikely, that someone has come to rely on the exception object'scommand
attribute having the exact value"git"
in this situation, it shouldarguably only be changed if it really is a bug for it to hold that value.
I believe it is a bug, for three reasons:
1. It is presented as the command line
When users see this in theGitCommandNotFound
exception message, I believe they will assume it is the command that was run:
cmdline: git
2. It differs from any otherGitCommandNotFound
In other situations that raiseGitCommandNotFound
, it comes fromGit.execute
, and the exception is constructed with the actual command line ascommand
(except that redactions are performed, such as for passwords):
Lines 1065 to 1079 ind28c20b
try: | |
proc=safer_popen( | |
command, | |
env=env, | |
cwd=cwd, | |
bufsize=-1, | |
stdin=(istreamorDEVNULL), | |
stderr=PIPE, | |
stdout=stdout_sink, | |
shell=shell, | |
universal_newlines=universal_newlines, | |
**subprocess_kwargs, | |
) | |
exceptcmd_not_found_exceptionaserr: | |
raiseGitCommandNotFound(redacted_command,err)fromerr |
3. The superclass documents thecommand
argument
GitCommandNotFound
inherits itscommand
attribute from itsCommandError
superclass. AlthoughCommandError
does not document thecommand
attribute directly, it does document thecommand
argument from which the value of that attribute is derived:
Lines 83 to 88 ind28c20b
classCommandError(GitError): | |
"""Base class for exceptions thrown at every stage of `Popen()` execution. | |
:param command: | |
A non-empty list of argv comprising the command-line. | |
""" |
This does not really guarantee the specific meaning of thecommand
attribute. (Likskov substitutability is usually a goal for objects that have already been constructed/initialized; subclass__new__
and__init__
are not expected to treat arguments the same ways as the superclass.) But it is a further reason to consider the current behavior inGit.refresh
unexpected.
Possible fixes
This could be fixed by havingGit.refresh
construct theGitCommandNotFoundError
with the same command line that itscls().version()
call causesGit.execute
to use, which is given byGIT_PYTHON_GIT_EXECUTABLE
.
Another approach, though, would be to keep a reference to theGitCommandNotFound
exception that occurred occurred due to runningcls().version()
:
Lines 381 to 385 ind28c20b
try: | |
cls().version() | |
has_git=True | |
except (GitCommandNotFound,PermissionError): | |
pass |
Then that same exception object, with its original information, could be reraised instead of constructing and raising a newGitCommandNotFound
object.
If this is to be done, then some refactoring may be in order, and a decision about whether thatGitCommandNotFound
object should be used as thecause
of anImportError
in the case that is raised should probably also be made:
Line 476 ind28c20b
raiseImportError(err) |
Thus, in addition to the other changes, that statement could be changed to theraise ... from ...
form.
There may be reasons to avoid this approach, in whole or in part, that I am not aware of.