mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(install): stop pinning managed checkouts to a single branch
`git clone --depth 1 --branch main` implies --single-branch, so every managed install ends up with remote.origin.fetch pinned to main. On such a checkout `git fetch origin <other>` updates FETCH_HEAD but never creates origin/<other>, leaving other branches invisible to `git branch -r` and `gh pr checkout`. install.sh made it stickier by re-narrowing the refspec on every run, undoing manual fixes. Restore the wildcard in both installers. It costs no bandwidth: naming the branch is what makes a fetch cheap, and every fetch we issue does.
This commit is contained in:
parent
f1af61354e
commit
4c602f2e05
3 changed files with 183 additions and 5 deletions
|
|
@ -1761,6 +1761,13 @@ function Install-Repository {
|
|||
# next `hermes update` checkout aborts on a "dirty" tree the user never
|
||||
# touched (see the update path above).
|
||||
git -c windows.appendAtomically=false config core.autocrlf false 2>$null
|
||||
# `clone --depth 1 --branch $Branch` implies --single-branch, which pins
|
||||
# remote.origin.fetch to that one branch: `git fetch origin <other>` then
|
||||
# updates FETCH_HEAD but never creates origin/<other>, so other branches
|
||||
# are invisible to `git branch -r` / `gh pr checkout` and `hermes update
|
||||
# --branch` fails. Restore the wildcard -- every fetch we issue names its
|
||||
# branch, so nothing extra gets downloaded.
|
||||
git -c windows.appendAtomically=false remote set-branches origin '*' 2>$null
|
||||
|
||||
# Post-clone pin: when a clone (or ZIP-fallback init) just landed us on
|
||||
# $Branch's tip, honour the higher-precedence $Commit / $Tag by checking
|
||||
|
|
|
|||
|
|
@ -305,6 +305,22 @@ EOF
|
|||
log_info "Discarded npm lockfile churn (${dirty_count} file(s))"
|
||||
}
|
||||
|
||||
# `git clone --depth 1 --branch main` implies --single-branch, which pins
|
||||
# remote.origin.fetch to main alone. On such a checkout `git fetch origin <b>`
|
||||
# updates FETCH_HEAD but never creates origin/<b>, so other branches are
|
||||
# invisible to `git branch -r` and `gh pr checkout`, and `hermes update
|
||||
# --branch <b>` fails with "does not exist locally or on origin".
|
||||
#
|
||||
# Restoring the wildcard costs nothing: every fetch the installer and the CLI
|
||||
# issue names its branch, and git only walks the full refspec on a bare
|
||||
# `git fetch`. Also heals checkouts an older installer already narrowed.
|
||||
widen_remote_branches() {
|
||||
local repo="${1:-$INSTALL_DIR}"
|
||||
[ -n "$repo" ] && [ -d "$repo/.git" ] || return 0
|
||||
command -v git >/dev/null 2>&1 || return 0
|
||||
git -C "$repo" remote set-branches origin '*' 2>/dev/null || true
|
||||
}
|
||||
|
||||
emit_manifest() {
|
||||
# Stage-Desktop is included only with --include-desktop, mirroring
|
||||
# install.ps1: the signed bootstrap installer (Hermes-Setup) passes it so
|
||||
|
|
@ -1210,11 +1226,11 @@ clone_repo() {
|
|||
autostash_ref="stash@{0}"
|
||||
fi
|
||||
|
||||
# Fetch only the target branch. A bare `git fetch origin` pulls
|
||||
# every ref, and this repo carries thousands of auto-generated
|
||||
# branches — on a non-single-branch checkout that turns each update
|
||||
# into a multi-minute download that can stall the installer.
|
||||
git remote set-branches origin "$BRANCH" 2>/dev/null || true
|
||||
widen_remote_branches "$INSTALL_DIR"
|
||||
# Naming the branch is what keeps the fetch cheap: a bare
|
||||
# `git fetch origin` pulls every ref, and this repo carries
|
||||
# thousands of branches, which turns each update into a
|
||||
# multi-minute download that can stall the installer.
|
||||
git fetch origin "$BRANCH"
|
||||
git checkout "$BRANCH"
|
||||
# Managed installs should follow origin/$BRANCH exactly. If the
|
||||
|
|
@ -1304,6 +1320,7 @@ EOF
|
|||
exit 1
|
||||
fi
|
||||
fi
|
||||
widen_remote_branches "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
|
|
|
|||
154
tests/test_managed_checkout_branch_visibility.py
Normal file
154
tests/test_managed_checkout_branch_visibility.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
"""Regression: managed installs must be able to see the repo's other branches.
|
||||
|
||||
``git clone --depth 1 --branch main`` implies ``--single-branch``, which pins
|
||||
``remote.origin.fetch`` to ``+refs/heads/main:refs/remotes/origin/main``. On
|
||||
such a checkout ``git fetch origin <other>`` updates FETCH_HEAD but never
|
||||
creates ``origin/<other>``, so other branches are invisible to ``git branch
|
||||
-r`` / ``gh pr checkout`` and ``hermes update --branch <other>`` fails with
|
||||
"does not exist locally or on origin".
|
||||
|
||||
The installer restores the wildcard refspec. That only costs bandwidth if
|
||||
something issues a bare ``git fetch``, so the startup update check has to keep
|
||||
naming its branch.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
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) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def _make_upstream(tmp_path: Path) -> Path:
|
||||
"""A bare remote with `main` plus a couple of side branches."""
|
||||
upstream = tmp_path / "upstream.git"
|
||||
seed = tmp_path / "seed"
|
||||
_git(tmp_path, "init", "--bare", "-b", "main", str(upstream))
|
||||
_git(tmp_path, "clone", upstream.as_uri(), str(seed))
|
||||
(seed / "a.txt").write_text("one\n")
|
||||
_git(seed, "add", "a.txt")
|
||||
_git(seed, "commit", "-m", "init")
|
||||
for branch in ("feat-1", "feat-2"):
|
||||
_git(seed, "checkout", "-b", branch, "main")
|
||||
(seed / f"{branch}.txt").write_text(branch)
|
||||
_git(seed, "add", ".")
|
||||
_git(seed, "commit", "-m", branch)
|
||||
_git(seed, "checkout", "main")
|
||||
_git(seed, "push", "--all", "origin")
|
||||
return upstream
|
||||
|
||||
|
||||
def _narrow_clone(tmp_path: Path, upstream: Path) -> Path:
|
||||
"""Clone exactly the way the installer does: shallow and single-branch."""
|
||||
checkout = tmp_path / "checkout"
|
||||
_git(tmp_path, "clone", "--depth", "1", "--branch", "main",
|
||||
upstream.as_uri(), str(checkout))
|
||||
assert _git(checkout, "config", "--get-all", "remote.origin.fetch") == (
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
)
|
||||
return checkout
|
||||
|
||||
|
||||
def _widen(checkout: Path) -> None:
|
||||
"""Run install.sh's widen_remote_branches() against a real checkout."""
|
||||
match = re.search(r"widen_remote_branches\(\) \{.*?\n\}", INSTALL_SH.read_text(),
|
||||
re.DOTALL)
|
||||
assert match is not None, "widen_remote_branches() not found in install.sh"
|
||||
script = f"{match.group(0)}\nwiden_remote_branches {shlex.quote(str(checkout))}\n"
|
||||
result = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_narrow_clone_cannot_resolve_other_branches(tmp_path: Path) -> None:
|
||||
"""The premise: without the fix, fetching a branch by name is not enough."""
|
||||
checkout = _narrow_clone(tmp_path, _make_upstream(tmp_path))
|
||||
|
||||
_git(checkout, "fetch", "origin", "feat-1")
|
||||
assert _git(checkout, "branch", "-r").split() == ["origin/main"]
|
||||
|
||||
# This is the exact pair of commands `hermes update --branch feat-1` runs.
|
||||
for args in (("checkout", "feat-1"), ("checkout", "-B", "feat-1", "origin/feat-1")):
|
||||
assert subprocess.run(["git", *args], cwd=checkout,
|
||||
capture_output=True).returncode != 0
|
||||
|
||||
|
||||
def test_widening_makes_other_branches_resolvable(tmp_path: Path) -> None:
|
||||
checkout = _narrow_clone(tmp_path, _make_upstream(tmp_path))
|
||||
_widen(checkout)
|
||||
|
||||
assert _git(checkout, "config", "--get-all", "remote.origin.fetch") == (
|
||||
"+refs/heads/*:refs/remotes/origin/*"
|
||||
)
|
||||
_git(checkout, "fetch", "--depth", "1", "origin", "feat-1")
|
||||
assert _git(checkout, "branch", "-r").split() == ["origin/feat-1", "origin/main"]
|
||||
_git(checkout, "checkout", "-B", "feat-1", "origin/feat-1")
|
||||
assert _git(checkout, "rev-parse", "--abbrev-ref", "HEAD") == "feat-1"
|
||||
|
||||
|
||||
def test_widening_downloads_nothing_by_itself(tmp_path: Path) -> None:
|
||||
"""Rewriting the refspec must not pull refs the user didn't ask for."""
|
||||
checkout = _narrow_clone(tmp_path, _make_upstream(tmp_path))
|
||||
before = _git(checkout, "rev-list", "--all", "--objects", "--count")
|
||||
|
||||
_widen(checkout)
|
||||
|
||||
assert _git(checkout, "branch", "-r").split() == ["origin/main"]
|
||||
assert _git(checkout, "rev-list", "--all", "--objects", "--count") == before
|
||||
|
||||
|
||||
def test_widening_is_idempotent(tmp_path: Path) -> None:
|
||||
"""Re-running the installer over an already-widened checkout is a no-op."""
|
||||
checkout = _narrow_clone(tmp_path, _make_upstream(tmp_path))
|
||||
_widen(checkout)
|
||||
_widen(checkout)
|
||||
|
||||
assert _git(checkout, "config", "--get-all", "remote.origin.fetch") == (
|
||||
"+refs/heads/*:refs/remotes/origin/*"
|
||||
)
|
||||
|
||||
|
||||
def test_update_check_stays_scoped_on_a_widened_checkout(tmp_path: Path) -> None:
|
||||
"""The startup banner check must not pull every branch once it can see them.
|
||||
|
||||
A bare ``git fetch origin`` on a widened checkout would drag in every
|
||||
branch — hundreds of MB on the real repo, behind a 10s timeout, on every
|
||||
launch. Naming main keeps the transfer to one ref.
|
||||
"""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
upstream = _make_upstream(tmp_path)
|
||||
checkout = _narrow_clone(tmp_path, upstream)
|
||||
_widen(checkout)
|
||||
|
||||
# Move main forward so the check has something to report.
|
||||
seed = tmp_path / "seed"
|
||||
(seed / "a.txt").write_text("two\n")
|
||||
_git(seed, "commit", "-am", "advance main")
|
||||
_git(seed, "push", "origin", "main")
|
||||
|
||||
assert banner._check_via_local_git(checkout) == banner.UPDATE_AVAILABLE_NO_COUNT
|
||||
assert _git(checkout, "branch", "-r").split() == ["origin/main"]
|
||||
assert _git(checkout, "rev-parse", "--is-shallow-repository") == "true"
|
||||
Loading…
Add table
Add a link
Reference in a new issue