fix(code): quiet expected non-repo git ls-files logging (#4701)

`_run_git_ls_files()` in `autocomplete.py` logged a debug line on every
nonzero exit, including the common non-repository case (exit 128) that
intentionally falls back to a glob scan. It now detects that expected
case via the `not a git repository` stderr marker and stays quiet, while
genuine failures still log the root/cwd, arguments, exit code, and
sanitized stderr for diagnosis. Existing autocomplete and fallback
behavior is unchanged.

Made by [Open
SWE](https://openswe.vercel.app/agents/90872166-fc98-ed05-3159-3a945178096d)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-13 16:09:50 -04:00
committed by GitHub
parent b19d8cc618
commit 3d499db8a6
2 changed files with 129 additions and 1 deletions
@@ -8,6 +8,7 @@ from __future__ import annotations
import asyncio
import logging
import os
import shutil
# S404: subprocess is required for git ls-files to get project file list
@@ -18,6 +19,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Protocol
from deepagents_code.project_utils import find_project_root
from deepagents_code.unicode_security import sanitize_control_chars
logger = logging.getLogger(__name__)
@@ -347,6 +349,17 @@ _MIN_FUZZY_SCORE = 15
_MIN_FUZZY_RATIO = 0.4
"""SequenceMatcher threshold for filename-only fuzzy matches."""
_NOT_A_REPO_MARKER = "not a git repository"
"""Marker in `git ls-files` stderr for a non-repository directory.
Running outside a work tree exits 128 and prints a "fatal: not a git
repository" message. That case intentionally falls back to a glob walk, so it
is left unlogged to avoid noise.
"""
_GIT_STDERR_LOG_LIMIT = 500
"""Max characters of git stderr to include in a diagnostic log line."""
def _run_git_ls_files(
git_path: str, root: Path, extra_args: list[str]
@@ -374,12 +387,26 @@ def _run_git_ls_files(
text=True,
timeout=5,
check=False,
# Git localizes stderr; use C so the non-repo marker stays stable.
env={**os.environ, "LC_ALL": "C"},
)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
logger.debug("git ls-files %s failed to run", extra_args, exc_info=True)
return False, []
if result.returncode != 0:
logger.debug("git ls-files %s exited with %d", extra_args, result.returncode)
stderr = sanitize_control_chars(result.stderr, max_length=_GIT_STDERR_LOG_LIMIT)
# Running outside a work tree exits 128 with a "not a git repository"
# fatal message. That is the expected trigger for the glob fallback, so
# keep it quiet. Everything else is a genuine failure worth logging with
# enough context (root/cwd, args, exit code, stripped stderr) to debug.
if _NOT_A_REPO_MARKER not in stderr.lower():
logger.debug(
"git ls-files failed: root=%s args=%s exit=%d stderr=%s",
root,
extra_args,
result.returncode,
stderr,
)
return False, []
return True, [f for f in result.stdout.strip().split("\n") if f]
@@ -1,6 +1,7 @@
"""Tests for autocomplete fuzzy search functionality."""
import asyncio
import logging
import subprocess
import threading
from pathlib import Path
@@ -1033,6 +1034,106 @@ class TestGetProjectFiles:
assert "a/b/c/d/e/deep.py" in files
def test_git_stderr_uses_stable_locale(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Git diagnostics must use the English locale expected by log filtering."""
class _Result:
returncode = 0
stdout = ""
stderr = ""
captured: dict[str, object] = {}
def fake_run(*_args: object, **kwargs: object) -> _Result:
captured.update(kwargs)
return _Result()
monkeypatch.setenv("LC_ALL", "fr_FR.UTF-8")
monkeypatch.setenv("AUTOCOMPLETE_TEST_ENV", "preserved")
monkeypatch.setattr(autocomplete_module.subprocess, "run", fake_run)
_run_git_ls_files("git", tmp_path, [])
env = cast("dict[str, str]", captured["env"])
assert env["LC_ALL"] == "C"
assert env["AUTOCOMPLETE_TEST_ENV"] == "preserved"
def test_non_repo_directory_is_quiet(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A non-Git directory (exit 128) must not emit a debug log.
Running `git ls-files` outside a work tree is the expected trigger for
the glob fallback, so the failure path stays silent.
"""
git_path = _get_git_executable()
assert git_path is not None
with caplog.at_level(logging.DEBUG, logger="deepagents_code"):
ok, files = _run_git_ls_files(git_path, tmp_path, [])
assert ok is False
assert files == []
assert caplog.records == []
def test_genuine_failure_logs_details(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A real Git failure logs root/cwd, args, exit code, and stderr."""
class _Result:
returncode = 129
stdout = ""
stderr = "fatal: unknown option `--bogus'\n"
def fake_run(*_args: object, **_kwargs: object) -> _Result:
return _Result()
monkeypatch.setattr(autocomplete_module.subprocess, "run", fake_run)
with caplog.at_level(logging.DEBUG, logger="deepagents_code"):
ok, files = _run_git_ls_files("git", tmp_path, ["--bogus"])
assert ok is False
assert files == []
assert len(caplog.records) == 1
message = caplog.records[0].getMessage()
assert str(tmp_path) in message
assert "--bogus" in message
assert "exit=129" in message
assert "unknown option" in message
def test_failure_stderr_is_sanitized(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Control characters in git stderr are neutralized before logging."""
class _Result:
returncode = 1
stdout = ""
stderr = "fatal: broken\x1b[31mred\r\nsecond line\n"
def fake_run(*_args: object, **_kwargs: object) -> _Result:
return _Result()
monkeypatch.setattr(autocomplete_module.subprocess, "run", fake_run)
with caplog.at_level(logging.DEBUG, logger="deepagents_code"):
_run_git_ls_files("git", tmp_path, [])
assert len(caplog.records) == 1
message = caplog.records[0].getMessage()
assert "\x1b" not in message
assert "\r" not in message
def test_glob_fallback_when_git_unavailable(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: