fix(install): disable auto maintenance in tree:0 clones (#127711)

This commit is contained in:
whyyagswhy
2026-09-29 06:14:38 -04:00
committed by Teknium
parent add654131f
commit 73c17151f6
3 changed files with 110 additions and 0 deletions

View File

@@ -126,6 +126,36 @@ def mark_unmarked_packs_promisor(repo_root: Path) -> int:
return marked
# A tree:0 partial clone that lets detached git maintenance auto write a
# commit-graph lazy-fetches every missing tree on demand, and each fetch
# re-triggers maintenance, looping without bound (#127711). A repo cloned or
# converted to tree:0 never needs automatic maintenance; explicit git gc and
# git maintenance run still work.
_TREE0_MAINTENANCE_OFF = (
("maintenance.auto", "false"),
("gc.auto", "0"),
("fetch.writeCommitGraph", "false"),
)
def disable_tree0_auto_maintenance(repo_root: Path) -> None:
"""Turn off automatic maintenance commit-graph writes in a tree:0 clone.
Never raises: a read-only config must not turn fetch recovery into a
traceback, matching mark_unmarked_packs_promisor above.
"""
for key, value in _TREE0_MAINTENANCE_OFF:
try:
subprocess.run(
["git", "config", key, value],
cwd=str(repo_root), check=True,
capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=30,
)
except Exception:
logger.warning("Could not set %s=%s in %s", key, value, repo_root)
def clear_stale_tmp_packs(repo_root: Path, *, min_age_seconds: Optional[int] = None) -> List[str]:
"""Remove aborted-transfer temp pack files; same contract as clear_stale_git_locks.
@@ -482,6 +512,7 @@ def fetch_full_commit_graph(repo_root: Path, *extra_refspecs: str, **run_kwargs)
# git writes the partial-clone config before it fetches, so a failed fetch converts too.
if converts:
mark_unmarked_packs_promisor(repo_root)
disable_tree0_auto_maintenance(repo_root)
return shallow

View File

@@ -473,6 +473,13 @@ stage_repository() {
: > "${pack%.pack}.promisor" || log_warn "could not mark $pack as a partial-clone pack"
fi
done
# Existing treeless checkout: same commit-graph lazy-fetch loop guard (#127711).
git -C "$INSTALL_DIR" config maintenance.auto false \
|| log_warn "could not disable maintenance.auto in $INSTALL_DIR"
git -C "$INSTALL_DIR" config gc.auto 0 \
|| log_warn "could not disable gc.auto in $INSTALL_DIR"
git -C "$INSTALL_DIR" config fetch.writeCommitGraph false \
|| log_warn "could not disable fetch.writeCommitGraph in $INSTALL_DIR"
fi
run_logged "Fetching origin/$BRANCH" git -C "$INSTALL_DIR" fetch origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH" \
|| fail "git fetch failed"
@@ -589,6 +596,15 @@ stage_repository() {
fail "cannot publish cloned checkout"
fi
rmdir "$staged"
# A treeless checkout plus detached git maintenance auto commit-graph
# writes lazy-fetch every missing tree in a loop (#127711). Automatic
# maintenance is never needed here; explicit git gc still works.
git -C "$INSTALL_DIR" config maintenance.auto false \
|| log_warn "could not disable maintenance.auto in $INSTALL_DIR"
git -C "$INSTALL_DIR" config gc.auto 0 \
|| log_warn "could not disable gc.auto in $INSTALL_DIR"
git -C "$INSTALL_DIR" config fetch.writeCommitGraph false \
|| log_warn "could not disable fetch.writeCommitGraph in $INSTALL_DIR"
log_success "Hermes Agent cloned"
fi
if [ -n "$INSTALL_COMMIT" ]; then

View File

@@ -296,3 +296,66 @@ def test_update_check_debris_cleanup_folds_lazy_fetch_packs(
lambda root: seen.setdefault("root", root) or 0)
check.clear_git_debris(partial_clone)
assert seen.get("root") == partial_clone
# ---- tree:0 auto-maintenance commit-graph loop (#127711) ----
#
# A tree:0 partial clone that lets detached ``git maintenance --auto`` write a
# commit-graph lazy-fetches every missing tree on demand; each fetch
# re-triggers maintenance, looping unboundedly (2000+ git procs observed).
# Repos we clone or convert to tree:0 must therefore disable the automatic
# maintenance paths. These pin the helper and its wiring into the conversion.
from hermes_cli.gitlock import ( # noqa: E402
disable_tree0_auto_maintenance,
fetch_full_commit_graph,
)
def _git_config(repo: Path, key: str) -> str:
result = subprocess.run(
["git", "config", "--get", key], cwd=str(repo),
capture_output=True, text=True, encoding="utf-8", errors="replace",
)
return result.stdout.strip()
def test_tree0_maintenance_off_lands_config(repo: Path) -> None:
disable_tree0_auto_maintenance(repo)
assert _git_config(repo, "maintenance.auto") == "false"
assert _git_config(repo, "gc.auto") == "0"
assert _git_config(repo, "fetch.writeCommitGraph") == "false"
def test_tree0_conversion_disables_auto_maintenance(
repo: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Tree:0 conversion still lands maintenance config."""
import hermes_cli.gitlock as gitlock
shallow = tmp_path / "shallow"
shallow.write_text("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n")
monkeypatch.setattr(gitlock, "_shallow_file_path", lambda _root: shallow)
monkeypatch.setattr(gitlock, "_partial_clone_filter", lambda _root, **_kw: None)
monkeypatch.setattr(gitlock, "_batch_missing_parents", lambda _root, _c: {"deadbeef"})
marked = []
monkeypatch.setattr(gitlock, "mark_unmarked_packs_promisor", lambda root: marked.append(root) or 0)
real_run = subprocess.run
def fake_run(argv, **kwargs):
if "fetch" in argv:
raise subprocess.CalledProcessError(1, argv, stderr="boom")
return real_run(argv, **kwargs)
monkeypatch.setattr(gitlock.subprocess, "run", fake_run)
with pytest.raises(subprocess.CalledProcessError):
fetch_full_commit_graph(repo, "origin", "main")
assert marked == [repo]
assert _git_config(repo, "maintenance.auto") == "false"
assert _git_config(repo, "gc.auto") == "0"
assert _git_config(repo, "fetch.writeCommitGraph") == "false"