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
13 changes: 7 additions & 6 deletions loopx/cli_commands/support_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ def register_support_control_commands(
registry_boundary_parser.add_argument(
"--require-not-tracked",
action="store_true",
help="Return non-zero if the registry is tracked while publication policy disallows pushing it.",
help=(
"For registries that cannot be pushed, return non-zero if tracked "
"or if Git cannot verify the tracking state."
),
)
registry_boundary_parser.add_argument(
"--require-gitignored",
Expand Down Expand Up @@ -309,14 +312,12 @@ def handle_support_control_command(
if (
args.require_not_tracked
and payload.get("ok")
and git.get("tracked")
and not payload.get("github_push_allowed")
and git.get("probe_status") == "git_unavailable"
and (git.get("inside_worktree") or git.get("worktree_discovery_unknown"))
):
payload = dict(payload)
payload["ok"] = False
payload.setdefault("risks", []).append(
"registry_tracked_but_not_push_allowed"
)
payload["error"] = "Could not determine whether the registry is tracked by Git."
if (
args.require_gitignored
and payload.get("ok")
Expand Down
36 changes: 16 additions & 20 deletions loopx/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,27 +195,23 @@ def run_git(*args: str, cwd: Path = probe_dir) -> subprocess.CompletedProcess[st
}

root = run_git("rev-parse", "--show-toplevel")
if root is None:
return unavailable()
inside = root.returncode == 0
tracked = False
ignored = False
if inside:
root_path = Path(root.stdout.strip())
try:
rel_path = os.path.relpath(path.resolve(), root_path)
except (OSError, ValueError):
rel_path = path.name
tracked_result = run_git("ls-files", "--error-unmatch", "--", rel_path, cwd=root_path)
ignored_result = run_git("check-ignore", "-q", "--", rel_path, cwd=root_path)
tracked = tracked_result is not None and tracked_result.returncode == 0
ignored = ignored_result is not None and ignored_result.returncode == 0
if root is None or root.returncode != 0:
return {**unavailable(), "worktree_discovery_unknown": True}

root_path = Path(root.stdout.strip())
try:
rel_path = os.path.relpath(path.resolve(), root_path)
except (OSError, ValueError):
rel_path = path.name
tracked_result = run_git("ls-files", "--error-unmatch", "--", rel_path, cwd=root_path)
ignored_result = run_git("check-ignore", "-q", "--", rel_path, cwd=root_path)
tracking_available = tracked_result is not None and tracked_result.returncode in (0, 1)
return {
"available": True,
"probe_status": "ok",
"inside_worktree": inside,
"tracked": tracked,
"ignored": ignored,
"available": tracking_available,
"probe_status": "ok" if tracking_available else "git_unavailable",
"inside_worktree": True,
"tracked": tracked_result is not None and tracked_result.returncode == 0,
"ignored": ignored_result is not None and ignored_result.returncode == 0,
"worktree_root_recorded": False,
}

Expand Down
40 changes: 40 additions & 0 deletions tests/test_registry_git_probe.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
import os
import signal
import subprocess
Expand Down Expand Up @@ -118,6 +119,44 @@ def unexpected_git(*_: object, **__: object) -> None:
}


@pytest.mark.parametrize("root_failure", [False, True], ids=["bad-index", "unknown-root"])
def test_registry_boundary_require_not_tracked_rejects_unavailable_git(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
root_failure: bool,
) -> None:
from loopx.cli import main

for variable in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"):
monkeypatch.delenv(variable, raising=False)
repo = tmp_path / "repo"
repo.mkdir()
run_git(repo, "init", "--quiet")
registry = repo / ".loopx" / "registry.json"
registry.parent.mkdir()
registry.write_text('{"goals": []}\n', encoding="utf-8")
run_git(repo, "add", "--force", "--", ".loopx/registry.json")
if root_failure:
monkeypatch.setenv("GIT_DIR", str(tmp_path / "missing.git"))
else:
bad_index = tmp_path / "bad-index"
bad_index.write_bytes(b"DIRC")
monkeypatch.setenv("GIT_INDEX_FILE", str(bad_index))

exit_code = main([
"--format", "json", "--registry", str(registry),
"--runtime-root", str(tmp_path / "runtime"), "registry-boundary", "--require-not-tracked",
])
payload = json.loads(capsys.readouterr().out)
assert exit_code == 1, payload
assert payload["ok"] is False
assert payload["git"]["inside_worktree"] is (not root_failure)
assert payload["git"].get("worktree_discovery_unknown", False) is root_failure
assert payload["git"]["probe_status"] == "git_unavailable"
assert "tracked by Git" in payload["error"]


@pytest.mark.skipif(os.name != "posix", reason="the probe's fake git is a POSIX shell script")
def test_registry_git_probe_retires_the_process_tree_on_timeout(
tmp_path: Path,
Expand All @@ -144,6 +183,7 @@ def test_registry_git_probe_retires_the_process_tree_on_timeout(

observed = registry_module._registry_git_probe(probe / "registry.json")
assert observed["probe_status"] == "git_unavailable"
assert observed["worktree_discovery_unknown"] is True

pid = int(grandchild.read_text(encoding="utf-8").strip())
deadline = time.monotonic() + 10
Expand Down
Loading