fix(install): disable auto maintenance in tree:0 clones (#127711)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user