Skip to content

fix: report a missing git instead of an unhandled FileNotFoundError - #620

Open
Eljees wants to merge 2 commits into
Bachmann1234:mainfrom
Eljees:fix/303-clear-error-when-git-is-missing
Open

fix: report a missing git instead of an unhandled FileNotFoundError#620
Eljees wants to merge 2 commits into
Bachmann1234:mainfrom
Eljees:fix/303-clear-error-when-git-is-missing

Conversation

@Eljees

@Eljees Eljees commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes #303.

Problem

With git not installed (or not on PATH), both entry points end in a traceback:

Traceback (most recent call last):
  ...
  File ".../diff_cover/command_runner.py", line 27, in execute
    with subprocess.Popen(command, stdout=stdout_pipe, stderr=stdout_pipe) as process:
FileNotFoundError: [Errno 2] No such file or directory: 'git'

Two things are going on:

  1. command_runner.execute puts subprocess.Popen(...) outside the try. The
    try only covers process.communicate(), so the FileNotFoundError that Popen
    raises when the executable does not exist escapes untouched.

  2. Both main() functions call GitPathTool.set_cwd(directory) before their own
    error handling starts. diff_quality_tool.main already has

    except OSError as exc:
        LOGGER.error("Failure: '%s'", str(exc))
        return 1

    and FileNotFoundError is an OSError — the handler for this case is already
    written, the failure just happens a few lines above the block that would catch it.
    diff_cover_tool.main has no handling at that point at all.

Precedent

This is the same class of defect as #378 ("Running diff-quality with a tool which is
not installed gives FileNotFoundError rather than helpful message"), fixed by #380.
That fix landed in run_command_for_code and covers "the linter is not installed";
the "git is not installed" path goes through execute and was left as it was.

Fix

  • ExecutableNotFoundError(CommandError) — a subclass, so git_diff.py's
    except CommandError and every other existing handler keep working unchanged.
  • execute() raises it for the FileNotFoundError from Popen only, with the message
    Improve error reporting when "git" is not found #303 asked for. The git-scm.com link is added only when the missing binary is git.
  • Both main() functions wrap set_cwd and report through LOGGER.error, returning 1.

The narrow catch is deliberate

My first version caught CommandError wholesale in main(), and two of your existing
tests rejected it —
tests/test_integration.py::TestDiffCoverIntegration::test_git_diff_error and
::TestDiffQualityIntegration::test_git_diff_error_diff_quality. Both set up a git that
runs and exits 1, and both require CommandError to propagate out of main(). That is
the project saying "the command ran and failed" and "there is no command to run" are
different things, which is where the separate subclass came from. Both tests pass here.

Tests

tests/test_command_runner_missing_executable.py, six cases: execute(), a non-git
binary getting no git link, the subclass relationship, both main() functions, and

def test_a_command_that_runs_and_fails_is_left_alone(failing_git):
    """Control. Green before and after: only a *missing* binary is reworded."""

which is green on both sides — that is what shows the error semantics did not move.

Verification

result
main (e34eec3, release 10.5.0), whole suite 382 passed, 0 failed
probe against the existing API before the fix 3 defects out of 3 — raw FileNotFoundError escaped execute() and both main()s
control before the fix (git present but failing) handled as CommandError — silent, as it must be
this branch, whole suite 388 passed, 0 failed
new failures against main none
reproducer from the issue, after the fix one line of text, exit code 1, no traceback
blocking lint (black, isort, doc8) clean

ruff and pylint are continue-on-error: true in verify.yaml, so I checked the
blocking three; diff-quality on this diff reports 100% for pyflakes.

…achmann1234#303)

subprocess.Popen sits outside the try in command_runner.execute, so when the
executable itself is missing the FileNotFoundError escapes raw. Both entry
points call GitPathTool.set_cwd before their own error handling begins --
diff_quality_tool.main already has `except OSError -> LOGGER.error -> return 1`
further down, it simply never gets the chance to run -- so `diff-cover` and
`diff-quality` print a traceback when git is not installed.

Add ExecutableNotFoundError as a subclass of CommandError, so anything that
already catches CommandError keeps working, and raise it from execute() only
for the FileNotFoundError that Popen throws. A command that runs and then
fails is untouched: tests/test_integration.py requires CommandError to escape
main() in that case, and it still does.

Both main() functions now wrap set_cwd and report the message through
LOGGER.error, returning 1.

This is the same class of fix as Bachmann1234#380 for Bachmann1234#378, which covered the "the linter
is not installed" path through run_command_for_code; the "git is not
installed" path through execute was left as it was.
Comment thread tests/test_command_runner_missing_executable.py Fixed
CodeQL flagged `assert "https://git-scm.com/" in message` as
py/incomplete-url-substring-sanitization. The rule is about URL checks done
with substring containment, and a test assertion is written the same way as
the pattern it warns about.

Import GIT_INSTALL_URL and assert on the whole trailing sentence instead,
which is a stricter assertion anyway, and use the constant for the negative
case as well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve error reporting when "git" is not found

2 participants