Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,21 @@ repos:
stages: [pre-commit]
description: "An inherited GIT_DIR makes --show-toplevel answer with the caller's CWD; resolve the root with autobot_shared.paths.git_repo_root()"

# #15246: a test that shells out to `git init`/`add`/`commit`/`checkout`/
# etc. in a tmp_path fixture, with subprocess.run's default of inheriting
# os.environ, does not touch that fixture at all under the pre-push hook
# -- it writes to whatever GIT_DIR names, which is the pushing worktree's
# own git directory. autobot_shared/paths_test.py did exactly that and
# dropped 86 tracked files from the live index (#15243 fixed the
# instance; this gates the class).
- id: git-write-env-scrubbed
name: "A test's git write must scrub GIT_DIR first (#15246)"
entry: tools/lint/check_git_write_env_scrubbed.py
language: python
files: (_test\.py|(^|/)test_[^/]+\.py)$
stages: [pre-commit]
description: "An inherited GIT_DIR makes a test fixture's git init/add/commit/etc. write to the real repository instead of tmp_path; pass env=scrubbed_git_env()"

# #14228: autobot-backend/requirements.txt ends with `-r ../requirements.txt`,
# so pip sees both files as one set and aborts when the same distribution
# arrives with different specifiers. openpyxl and python-pptx did exactly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import pytest

from autobot_shared.paths import scrubbed_git_env
from services.knowledge.code_indexer import (
_CACHE_VERSION_KEY,
_EXTRACTORS,
Expand Down Expand Up @@ -346,20 +347,36 @@ def test_index_directory_writes_a_record_reflecting_the_tree_it_walked(tmp_path)
# ------------------------------------------------- "how stale is this graph?"


def _test_git_env() -> dict[str, str]:
"""#15246: env for every git subprocess this fixture spawns.

Scrubbed rather than os.environ: the pre-push hook runs this suite with
GIT_DIR pointing at the worktree it is pushing (every checkout here is
one), and an unscrubbed `git init`/`git add`/`git commit` here would
then operate on THAT repository instead of tmp_path's. See
autobot_shared/paths_test.py and #15246 for the reproduced incident.
"""
return {**scrubbed_git_env(), "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}


@pytest.fixture
def git_repo(tmp_path):
"""A real two-commit repo — the distance question is about git, so use git."""
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "a@b.c"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "t"], check=True)
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True, env=_test_git_env())
subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "a@b.c"], check=True, env=_test_git_env())
subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "t"], check=True, env=_test_git_env())
(tmp_path / "f.txt").write_text("1\n", encoding="utf-8")
subprocess.run(["git", "-C", str(tmp_path), "add", "-A"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "one"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "add", "-A"], check=True, env=_test_git_env())
subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "one"], check=True, env=_test_git_env())
first = subprocess.run(
["git", "-C", str(tmp_path), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
["git", "-C", str(tmp_path), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
env=_test_git_env(),
).stdout.strip()
(tmp_path / "f.txt").write_text("2\n", encoding="utf-8")
subprocess.run(["git", "-C", str(tmp_path), "commit", "-qam", "two"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "commit", "-qam", "two"], check=True, env=_test_git_env())
return tmp_path, first


Expand All @@ -377,7 +394,7 @@ def test_a_consumer_learns_the_distance_without_rewalking_the_repo(git_repo):
def test_a_current_graph_reports_zero_not_unknown(git_repo):
repo, _ = git_repo
head = subprocess.run(
["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True, check=True, env=_test_git_env()
).stdout.strip()

assert _behind({_PROVENANCE_ID: _provenance(indexed_at_commit=head, root_dir=str(repo))}, repo) == 0
Expand All @@ -402,7 +419,7 @@ def test_a_graph_built_by_another_extractor_version_is_unanswerable(git_repo):
"""
repo, _ = git_repo
head = subprocess.run(
["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True, check=True, env=_test_git_env()
).stdout.strip()
record = _provenance(indexed_at_commit=head, root_dir=str(repo), extractor_version=EXTRACTOR_VERSION + 1)

Expand Down
16 changes: 8 additions & 8 deletions autobot-backend/services/knowledge/code_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

from autobot_shared.code_graph import ResolvedCall, compute_node_id, module_path_from_rel_path, resolve_call
from autobot_shared.logging_manager import get_logger
from autobot_shared.paths import scrubbed_git_env
from constants.path_constants import PATH
from utils.file_categorization import (
ALL_CODE_EXTENSIONS,
Expand Down Expand Up @@ -989,14 +990,13 @@ def _git(root_dir: str, *args: str) -> str:
fail an index run that otherwise succeeded.
"""
try:
completed = (
subprocess.run( # nosec B603 B607 # fixed argv, shell=False, no user-supplied option can be injected
["git", "-C", root_dir, *args],
capture_output=True,
text=True,
timeout=_GIT_TIMEOUT_SECONDS,
check=False,
)
completed = subprocess.run( # nosec B603 B607 # fixed argv, no shell, no injection
["git", "-C", root_dir, *args],
capture_output=True,
text=True,
timeout=_GIT_TIMEOUT_SECONDS,
check=False,
env=scrubbed_git_env(), # #15246: an inherited GIT_DIR would override -C
)
except (OSError, subprocess.SubprocessError) as exc:
logger.debug("code_indexer: git %s failed in %s: %s", args, root_dir, exc)
Expand Down
11 changes: 11 additions & 0 deletions autobot-backend/services/task_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from typing import Any, Iterator, Optional

from autobot_shared.logging_manager import get_logger
from autobot_shared.paths import scrubbed_git_env
from autobot_shared.time_utils import utc_timestamp
from services.task_workspace_common import validate_task_id

Expand Down Expand Up @@ -216,6 +217,11 @@ def release(
check=True,
capture_output=True,
text=True,
# #15246: an inherited GIT_DIR (a caller invoked from inside a git
# hook, or a test with one ambient from the pre-push hook running
# this suite) overrides cwd, so this can silently act on the WRONG
# repository -- worktree removal AND the branch -D below it.
env=scrubbed_git_env(),
)
# Best-effort branch deletion — ignore failures (e.g. branch pushed / not
# found). Cleanup/eviction passes force_branch=False → ``-d`` keeps a
Expand All @@ -225,6 +231,7 @@ def release(
cwd=str(root),
check=False,
capture_output=True,
env=scrubbed_git_env(), # #15246: see the worktree-remove call above
)
logger.info("Released workspace for task=%s", task_id)
except subprocess.CalledProcessError as exc:
Expand Down Expand Up @@ -347,6 +354,9 @@ def _git_add_worktree(root: Path, workspace_dir: Path, branch: str) -> None:
check=True,
capture_output=True,
text=True,
# #15246: see _git_add_worktree's sibling call below and release()'s
# worktree-remove above -- an inherited GIT_DIR overrides cwd here too.
env=scrubbed_git_env(),
)
except subprocess.CalledProcessError as exc:
stderr = exc.stderr or ""
Expand All @@ -359,6 +369,7 @@ def _git_add_worktree(root: Path, workspace_dir: Path, branch: str) -> None:
check=True,
capture_output=True,
text=True,
env=scrubbed_git_env(), # #15246: see the first worktree-add call above
)
except subprocess.CalledProcessError as exc2:
stderr2 = exc2.stderr or ""
Expand Down
38 changes: 26 additions & 12 deletions autobot-backend/tasks/test_task_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

import pytest

from autobot_shared.paths import scrubbed_git_env


def _load_tw():
"""Load task_workspace directly, bypassing the services package stub."""
Expand All @@ -40,26 +42,38 @@ def _load_tw():
cleanup_stale = _tw.cleanup_stale


def _test_git_env() -> dict[str, str]:
"""#15246: env for every git subprocess this suite spawns.

Scrubbed rather than os.environ: the pre-push hook runs this suite with
GIT_DIR pointing at the worktree it is pushing (every checkout here is
one), and an unscrubbed `git init`/`git add`/`git commit` here would
then operate on THAT repository instead of tmp_path's. See
autobot_shared/paths_test.py and #15246 for the reproduced incident.
"""
return {**scrubbed_git_env(), "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}


@pytest.fixture()
def git_repo(tmp_path: Path) -> Path:
"""Initialise a minimal git repo in tmp_path with one initial commit."""
subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True)
subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True, env=_test_git_env())
subprocess.run(
["git", "-C", str(tmp_path), "config", "user.email", "test@test.com"],
check=True,
capture_output=True,
env=_test_git_env(),
)
subprocess.run(
["git", "-C", str(tmp_path), "config", "user.name", "Test"],
check=True,
capture_output=True,
env=_test_git_env(),
)
(tmp_path / "README.md").write_text("test repo")
subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True, capture_output=True)
subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True, capture_output=True, env=_test_git_env())
subprocess.run(
["git", "-C", str(tmp_path), "commit", "-m", "init"],
check=True,
capture_output=True,
["git", "-C", str(tmp_path), "commit", "-m", "init"], check=True, capture_output=True, env=_test_git_env()
)
return tmp_path

Expand Down Expand Up @@ -126,11 +140,9 @@ def test_workspace_dir_stable_across_heartbeats(self, git_repo: Path) -> None:
ws1 = allocate(task_id, "agent-resume", repo_root=git_repo)
wt = Path(ws1.worktree_path)
(wt / "heartbeat_1.txt").write_text("tick 1")
subprocess.run(["git", "-C", str(wt), "add", "."], check=True, capture_output=True)
subprocess.run(["git", "-C", str(wt), "add", "."], check=True, capture_output=True, env=_test_git_env())
subprocess.run(
["git", "-C", str(wt), "commit", "-m", "hb1"],
check=True,
capture_output=True,
["git", "-C", str(wt), "commit", "-m", "hb1"], check=True, capture_output=True, env=_test_git_env()
)

ws2 = allocate(task_id, "agent-resume", repo_root=git_repo)
Expand Down Expand Up @@ -325,12 +337,14 @@ def _backdate(workspace_dir: Path) -> None:

def _commit_in_worktree(wt: Path, msg: str) -> None:
(wt / "work.txt").write_text(msg)
subprocess.run(["git", "-C", str(wt), "add", "."], check=True, capture_output=True)
subprocess.run(["git", "-C", str(wt), "commit", "-m", msg], check=True, capture_output=True)
subprocess.run(["git", "-C", str(wt), "add", "."], check=True, capture_output=True, env=_test_git_env())
subprocess.run(["git", "-C", str(wt), "commit", "-m", msg], check=True, capture_output=True, env=_test_git_env())


def _branch_exists(git_repo: Path, branch: str) -> bool:
r = subprocess.run(["git", "-C", str(git_repo), "branch", "--list", branch], capture_output=True, text=True)
r = subprocess.run(
["git", "-C", str(git_repo), "branch", "--list", branch], capture_output=True, text=True, env=_test_git_env()
)
return bool(r.stdout.strip())


Expand Down
30 changes: 19 additions & 11 deletions autobot-infrastructure/shared/scripts/hooks/lib/_common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,44 @@

from __future__ import annotations

import os
import subprocess
from pathlib import Path

import pytest

from autobot_shared.paths import scrubbed_git_env

LIB_PATH = Path(__file__).resolve().parent / "_common.sh"


def _test_git_env() -> dict[str, str]:
"""#15246: env for every git subprocess this suite spawns.

Scrubbed rather than os.environ: the pre-push hook runs this suite with
GIT_DIR pointing at the worktree it is pushing (every checkout here is
one), and an unscrubbed `git init`/`git add`/`git commit` in a
fixture then operates on THAT repository instead of tmp_path's. See
autobot_shared/paths_test.py and #15246 for the reproduced incident.
"""
return {**scrubbed_git_env(), "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}


def _run_in_subshell(script: str, cwd: Path | None = None) -> subprocess.CompletedProcess:
"""Source _common.sh and run a snippet; capture stdout/stderr/exit."""
full = f'source "{LIB_PATH}"\n{script}'
return subprocess.run(
["bash", "-c", full],
capture_output=True,
text=True,
cwd=cwd,
)
return subprocess.run(["bash", "-c", full], capture_output=True, text=True, cwd=cwd, env=_test_git_env())


def _make_git_repo(tmp_path: Path, files: dict[str, str]) -> Path:
"""Initialize a git repo at tmp_path with `files` staged."""
subprocess.run(["git", "init", "--quiet"], cwd=tmp_path, check=True)
subprocess.run(["git", "config", "user.email", "test@test"], cwd=tmp_path, check=True)
subprocess.run(["git", "config", "user.name", "test"], cwd=tmp_path, check=True)
subprocess.run(["git", "init", "--quiet"], cwd=tmp_path, check=True, env=_test_git_env())
subprocess.run(["git", "config", "user.email", "test@test"], cwd=tmp_path, check=True, env=_test_git_env())
subprocess.run(["git", "config", "user.name", "test"], cwd=tmp_path, check=True, env=_test_git_env())
for rel, content in files.items():
path = tmp_path / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
subprocess.run(["git", "add", rel], cwd=tmp_path, check=True)
subprocess.run(["git", "add", rel], cwd=tmp_path, check=True, env=_test_git_env())
return tmp_path


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,29 @@

from __future__ import annotations

import os
import stat
import subprocess
from pathlib import Path

from autobot_shared.paths import scrubbed_git_env

HOOK_PATH = Path(__file__).resolve().parent / "pre-commit-branch-guard"


def _test_git_env() -> dict[str, str]:
"""#15246: env for every git subprocess this suite spawns.

Scrubbed rather than os.environ: the pre-push hook runs this suite with
GIT_DIR pointing at the worktree it is pushing (every checkout here is
one), and an unscrubbed `git init`/`git add`/`git commit` in a
fixture then operates on THAT repository instead of tmp_path's. See
autobot_shared/paths_test.py and #15246 for the reproduced incident.
"""
return {**scrubbed_git_env(), "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null"}


def _git(repo: Path, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True, check=True)
return subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True, check=True, env=_test_git_env())


def _init_repo(tmp_path: Path, branch: str = "feature") -> Path:
Expand All @@ -48,14 +61,14 @@ def _write_record(repo: Path, branch: str) -> None:
def test_matching_branch_allows_commit(tmp_path: Path) -> None:
repo = _init_repo(tmp_path, branch="feature")
_write_record(repo, "feature")
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True)
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True, env=_test_git_env())
assert result.returncode == 0, result.stdout + result.stderr


def test_mismatched_branch_blocks_commit(tmp_path: Path) -> None:
repo = _init_repo(tmp_path, branch="feature")
_write_record(repo, "main")
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True)
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True, env=_test_git_env())
assert result.returncode != 0, result.stdout + result.stderr
assert "COMMIT ABORTED" in result.stdout

Expand All @@ -64,7 +77,7 @@ def test_missing_record_file_allows_commit(tmp_path: Path) -> None:
"""No record file means pre-commit-record-branch didn't run — skip
silently rather than block (e.g. first install)."""
repo = _init_repo(tmp_path, branch="feature")
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True)
result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True, env=_test_git_env())
assert result.returncode == 0, result.stdout + result.stderr


Expand All @@ -83,7 +96,7 @@ def test_a_missing_common_lib_does_not_report_clean(self, tmp_path: Path) -> Non
hook_copy.write_bytes(HOOK_PATH.read_bytes())
hook_copy.chmod(0o755)

result = subprocess.run([str(hook_copy)], cwd=repo, capture_output=True, text=True)
result = subprocess.run([str(hook_copy)], cwd=repo, capture_output=True, text=True, env=_test_git_env())
assert result.returncode != 0, "the hook reported clean with no dependency and a real mismatch present"


Expand Down Expand Up @@ -123,7 +136,7 @@ def test_a_git_branch_failure_does_not_report_clean(self, tmp_path: Path) -> Non
_write_record(repo, "main") # a genuine mismatch present

fake_bin = self._make_fake_git(tmp_path)
env = dict(os.environ)
env = dict(_test_git_env())
env["PATH"] = f"{fake_bin}:{env.get('PATH', '')}"

result = subprocess.run(["bash", str(HOOK_PATH)], cwd=repo, capture_output=True, text=True, env=env)
Expand Down
Loading
Loading