[Bug] GitCommandNotFound when executing repo.git.execute on macOS
Describe the bug When executing repo.git.execute with string-type arguments on macOS, it throws a GitCommandNotFound exception:
Traceback (most recent call last):
File "/Users/macuser/.pyenv/versions/3.12.2/lib/python3.12/site-packages/git/cmd.py", line 1262, in execute
proc = safer_popen(
^^^^^^^^^^^^
File "/Users/macuser/.pyenv/versions/3.12.2/lib/python3.12/subprocess.py", line 1026, in __init__
self._execute_child(args, executable, preexec_fn, close_fds,
File "/Users/macuser/.pyenv/versions/3.12.2/lib/python3.12/subprocess.py", line 1953, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'git log -n 1'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/macuser/test.py", line 36, in <module>
cmd = repo.git.execute("git log -n 1")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/macuser/.pyenv/versions/3.12.2/lib/python3.12/site-packages/git/cmd.py", line 1275, in execute
raise GitCommandNotFound(redacted_command, err) from err
git.exc.GitCommandNotFound: Cmd('g') not found due to: FileNotFoundError('[Errno 2] No such file or directory: 'git log -n 1'')
I have installed git and it works well on other commands, only repo.git.execute doesn't work'
The error only occurs when passing a string-type argument to repo.git.execute
Works correctly when using string array arguments
Works correctly when using shell=True argument
for example:
# Failing case (string argument)
repo.git.execute("git log -n 1") # Throws exception
# Working case (array argument)
repo.git.execute(["git", "log", "-n", "1"]) # Executes successfully
# Working case (array argument)
repo.git.execute("git log -n 1", shell=True) # Executes successfully
Environment
- Python version: 3.12
- GitPython version: 3.1.43
- Operating System: macOS 14.3.1 (Sonoma)
- Git version: 2.46.0
- By the way, I'm using pyenv
The documentation does make it appear as if a string should be working even though I am pretty sure it's never used that way.
Maybe the documentation should be updated.
The way this should be used is like this: repo.git.log(n=1).
The documentation does make it appear as if a string should be working even though I am pretty sure it's never used that way.
Maybe the documentation should be updated.
The way this should be used is like this:
repo.git.log(n=1).
git log is only an example, I use other commands, such as git merge-base
It works well on Windows, only get error on macOS. (I didn‘t test on Linux)
There is at least one bug shown here. The exception message GitPython produces is wrong and misleading, because at no point was an attempt made to execute a command called g: no such command was intended, and GitPython also did not actually try to run a command called that. GitPython forms the exception message in such a way that assumes the first element of the command is the command name. That is correct when the command is a list, but incorrect when the command is a string. I think this could and should be fixed.
But it looks like this issue is not only about the exception message, and that this is mainly reporting the inability to run repo.git.execute("git log -n 1") successfully on macOS as a bug. But such a command is incorrect except on Windows, because it is only on Windows that automatic word splitting of command lines is well-defined as part of the semantics of running commands even in the absence of a shell.
The execute method of Git objects uses the Python standard-library subprocess module to run commands. All the facilities in subprocess exhibit this same behavior. On Unix-like systems (including macOS), passing a single string as a command attempts to run an executable of that name. If the string has characters in it that would be treated specially in a shell (or informally), such as spaces, those characters are not treated specially. They are merely taken to be part of that name. For example, this works because ls is the command name:
ek@sup:~$ python3.12
Python 3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> subprocess.run('ls')
bin repos snap src
CompletedProcess(args='ls', returncode=0)
But this does not work, because it tries to run a command whose name is ls -l, rather than a command whose name is ls with an argument -l as might be intended (and as it would mean on Windows):
>>> subprocess.run('ls -l')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.12/subprocess.py", line 548, in run
with Popen(*popenargs, **kwargs) as process:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/subprocess.py", line 1026, in __init__
self._execute_child(args, executable, preexec_fn, close_fds,
File "/usr/lib/python3.12/subprocess.py", line 1955, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'ls -l'
(The error message there is directly analogous to the [Errno 2] No such file or directory: 'git log -n 1' part of the error message you encountered, which reveals that an attempt was made to find an executable whose name was the whole string git log -n 1.)
And this works because it passes a list of the command and arguments rather than a string, so it unambiguously runs ls with the argument -l:
>>> subprocess.run(['ls', '-l'])
total 16
drwxrwxr-x 2 ek ek 4096 Nov 3 07:58 bin
drwxrwxr-x 9 ek ek 4096 Nov 3 08:12 repos
drwx------ 4 ek ek 4096 Nov 28 13:04 snap
drwxrwxr-x 5 ek ek 4096 Mar 17 10:32 src
CompletedProcess(args=['ls', '-l'], returncode=0)
The Python documentation on subprocess.Popen covers this behavior and how it differs between Windows systems and Unix-like systems, with the most relevant part being:
On POSIX, if args is a string, the string is interpreted as the name or path of the program to execute. However, this can only be done if not passing arguments to the program.
Both when using something like subprocess.Popen or subprocess.run directly, and when using repo.git.execute on a Repo object repo, usually the best thing to do would be to use a list. As you found and showed, this works on all systems:
# Working case (array argument)
repo.git.execute(["git", "log", "-n", "1"]) # Executes successfully
However, with GitPython. when the command you are running is git, usually you would use a dynamic method repo.git.log(n=1), as shown in https://github.com/gitpython-developers/GitPython/issues/2016#issuecomment-2736541433, instead of execute.
(If for some reason you want to pass all arguments explicitly, this can still be done that way: repo.git.log('-n', '1'). I think that is only infrequently needed, though. In this case it is equivalent to using the n=1 keyword argument.)