mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 09:45:24 -04:00
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user