Handle interrupted or incomplete install/clone
Cluster 40998 · hermes cli. Data as of
Nothing open on GitHub. Fix merged in #41941 on 8 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #41941. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #41941. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items address interrupted checkouts that leave a commit-less state, triggering re-cloning rather than an error, alongside a reported Windows installation issue. The common thread is install-time errors and recovery behavior.
Open issues0
Open PRs0
Fixes on record
-
#41941 fix(install): re-clone interrupted (commit-less) checkout instead of failing update
files:
scripts/install.ps1,scripts/install.sh,tests/test_install_no_initial_commit.pyShow the diff
-
scripts/install.ps1@@ -1060,9 +1060,10 @@ function Install-Repository { # directory OR a symlink OR a submodule-style gitfile -- and also when # it's a broken stub left over from a failed previous install (e.g. # a partial Remove-Item that couldn't delete a locked index.lock). - # Validate the repo properly by asking git itself. Two checks - # belt-and-braces: rev-parse AND git status. If either fails the - # repo is broken and we fall through to a fresh clone. + # Validate the repo properly by asking git itself. Three checks + # belt-and-braces: rev-parse (work tree), git status, and a resolvable + # HEAD (an initial commit). If any fails the repo is broken and we + # fall through to a fresh clone. $repoValid = $false if (Test-Path "$InstallDir\.git") { Push-Location $InstallDir @@ -1077,7 +1078,17 @@ function Install-Repository { $null = & git -c windows.appendAtomically=false status --short 2>&1 $statusOk = ($LASTEXITCODE -eq 0) - if ($revParseOk -and $statusOk) { + # An interrupted previous clone leaves a repo with NO initial + # commit. rev-parse/status still succeed there, but the update + # path's `git stash` (and later `git checkout`) abort with + # "You do not have the initial commit yet" and fail the install + # (#40998). Require a resolvable HEAD so such partial checkouts + # are treated as broken and re-cloned fresh below. + $global:LASTEXITCODE = 0 + $null = & git -c windows.appendAtomically=false rev-parse --verify HEAD 2>&1 + $hasCommit = ($LASTEXITCODE -eq 0) + + if ($revParseOk -and $statusOk -and $hasCommit) { $repoValid = $true } } catch {} @@ -1198,16 +1209,19 @@ function Install-Repository { } $didUpdate = $true } else { - # Directory exists but isn't a usable git repo. Wipe it and - # fall through to a fresh clone. A leftover ``.git`` stub from - # a partial uninstall used to lock the installer into the - # "update" branch forever, emitting three ``fatal: not a git - # repository`` errors and failing with "not in a git directory". - Write-Warn "Existing directory at $InstallDir is not a valid git repo -- replacing it." + # Directory exists but isn't a usable git repo -- e.g. an + # interrupted clone with no initial commit (#40998), or a leftover + # ``.git`` stub from a partial uninstall that used to lock the + # installer into the "update" branch forever. Move it aside rather + # than deleting it -- never destroy a directory the user might still + # want -- and fall through to a fresh clone. + $backupDir = "$InstallDir.broken-" + (Get-Date -Format "yyyyMMdd-HHmmss") + Write-Warn "Existing directory at $InstallDir is not a valid git repo." + Write-Warn "Moving it aside to $backupDir before re-cloning." try { - Remove-Item -Recurse -Force $InstallDir -ErrorAction Stop + Move-Item -LiteralPath $InstallDir -Destination $backupDir -ErrorAction Stop } catch { - Write-Err "Could not remove $InstallDir : $_" + Write-Err "Could not move $InstallDir aside : $_" Write-Info "Close any programs that might be using files in $InstallDir (editors," Write-Info "terminals, running hermes processes) and try again." throw -
scripts/install.sh@@ -1092,6 +1092,18 @@ show_manual_install_hint() { clone_repo() { log_info "Installing to $INSTALL_DIR..." + # An interrupted previous clone leaves a .git with no initial commit, where + # the update path's `git stash` / `git checkout` abort with "You do not + # have the initial commit yet" and fail the install (#40998). Move such a + # partial checkout aside -- never delete it, in case it holds something the + # user wants -- so the fresh-clone path below can proceed. + if [ -d "$INSTALL_DIR/.git" ] && ! git -C "$INSTALL_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then + backup_dir="${INSTALL_DIR}.broken-$(date -u +%Y%m%d-%H%M%S)" + log_warn "Existing checkout at $INSTALL_DIR has no commits (interrupted clone)." + log_warn "Moving it aside to $backup_dir before re-cloning." + mv "$INSTALL_DIR" "$backup_dir" + fi + if [ -d "$INSTALL_DIR" ]; then if [ -d "$INSTALL_DIR/.git" ]; then log_info "Existing installation found, updating..." -
tests/test_install_no_initial_commit.py@@ -0,0 +1,136 @@ +"""Regression for #40998: installer fails on an interrupted prior clone. + +A previous clone that died before its first commit leaves ``$INSTALL_DIR/.git`` +present but with no resolvable ``HEAD``. ``git rev-parse --is-inside-work-tree`` +and ``git status`` both still succeed there, so the installer treated it as a +valid checkout and tried to *update* it -- but ``git stash``/``git checkout`` +abort with "You do not have the initial commit yet", failing the install at the +"Cloning Hermes repository" stage. + +Both installers must instead treat a commit-less checkout as broken and +re-clone fresh. +""" + +from __future__ import annotations + +import re +import shlex +import shutil +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" +INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1" + +pytestmark = pytest.mark.skipif( + shutil.which("git") is None or shutil.which("bash") is None, + reason="needs git and bash", +) + + +def _git(cwd: Path, *args: str) -> None: + subprocess.run( + ["git", "-c", "user.email=t@t", "-c", "user.name=t", *args], + cwd=cwd, + check=True, + capture_output=True, + ) + + +def _extract_no_commit_guard() -> str: + """Pull the clone_repo() guard that drops a commit-less checkout.""" + text = INSTALL_SH.read_text() + m = re.search( + r'if \[ -d "\$INSTALL_DIR/\.git" \] && ! git -C "\$INSTALL_DIR" ' + r"rev-parse --verify HEAD.*?\n fi", + text, + re.DOTALL, + ) + assert m is not None, "no-commit guard not found in install.sh clone_repo()" + return m.group(0) + + +def _run_guard(install_dir: Path) -> None: + block = _extract_no_commit_guard() + script = ( + "log_warn() { echo \"WARN: $*\"; }\n" + f"INSTALL_DIR={shlex.quote(str(install_dir))}\n" + f"{block}\n" + ) + res = subprocess.run(["bash", "-c", script], capture_output=True, text=True) + assert res.returncode == 0, res.stderr + + +def test_install_sh_guard_moves_commitless_checkout_aside(tmp_path: Path) -> None: + install_dir = tmp_path / "hermes-agent" + install_dir.mkdir() + _git(install_dir, "init") + (install_dir / "leftover.txt").write_text("partial download") # untracked + + # Sanity: this is exactly the state that breaks `git stash`. + head = subprocess.run( + ["git", "-C", str(install_dir), "rev-parse", "--verify", "HEAD"], + capture_output=True, + ) + assert head.returncode != 0 + + _run_guard(install_dir) + # The original path is cleared so a fresh clone can proceed, but the + # content is preserved in a backup (never deleted -- review feedback). + assert not install_dir.exists(), "commit-less checkout should be moved aside" + backups = list(install_dir.parent.glob(install_dir.name + ".broken-*")) + assert len(backups) == 1, "broken checkout should be moved to one backup dir" + assert (backups[0] / "leftover.txt").read_text() == "partial download" + + +def test_install_sh_guard_keeps_repo_with_commits(tmp_path: Path) -> None: + install_dir = tmp_path / "hermes-agent" + install_dir.mkdir() + _git(install_dir, "init") + (install_dir / "f.txt").write_text("real content") + _git(install_dir, "add", "f.txt") + _git(install_dir, "commit", "-m", "init") + + _run_guard(install_dir) + assert install_dir.exists() + assert (install_dir / "f.txt").exists(), "a real checkout must be left intact" + assert not list(install_dir.parent.glob(install_dir.name + ".broken-*")), ( + "a healthy checkout must not be moved aside" + ) + + +def test_install_sh_guard_ignores_non_repo_dir(tmp_path: Path) -> None: + install_dir = tmp_path / "hermes-agent" + install_dir.mkdir() + (install_dir / "f.txt").write_text("not a repo") + + _run_guard(install_dir) + # No .git → not our concern; the existing "not a git repository" branch + # still handles it. The guard must leave it untouched. + assert install_dir.exists() + assert (install_dir / "f.txt").exists() + + +def test_install_ps1_validity_requires_initial_commit() -> None: + """The PowerShell repo-validity gate must also require a resolvable HEAD.""" + text = INSTALL_PS1.read_text() + assert "rev-parse --verify HEAD" in text, ( + "install.ps1 must probe for an initial commit (#40998)" + ) + # Contract: $repoValid is only set when the HEAD probe succeeded too. + assert re.search( + r"if \(\$revParseOk -and \$statusOk -and \$hasCommit\) \{", + text, + ), "repo validity must be gated on $hasCommit, not just rev-parse + status" + # Cleanup must be non-destructive: move the broken checkout aside, never + # `Remove-Item -Recurse -Force` it (review feedback on #40998). + assert "Move-Item -LiteralPath $InstallDir" in text, ( + "install.ps1 must move an invalid checkout aside, not delete it" + ) + assert "Remove-Item -Recurse -Force $InstallDir -ErrorAction Stop" not in text, ( + "the destructive wipe of an existing install dir must be gone " + "(transient cleanup of a just-failed clone is fine)" + )
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#41208 fix(install): re-clone interrupted (commit-less) checkout instead of failing update · merged via another change →
#41941,#40998
Closed issues · 1
- Closed#40998[Bug]: windows安装包安装的时候安装失败
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #41941
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 8 Jun 2026