fix(update): widen the remote refspec so --branch can resolve

On a single-branch checkout the scoped fetch updates FETCH_HEAD but not
origin/<branch>, so `hermes update --branch <other>` died on the
follow-up checkout with "does not exist locally or on origin", and
`--check --branch <other>` reported the branch missing from a remote
that has it. Widening here means existing installs self-heal on the next
update instead of needing a reinstall.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 20:11:18 -05:00
parent 4c602f2e05
commit 50b700c56d
2 changed files with 41 additions and 0 deletions

View file

@ -10741,6 +10741,27 @@ def _resolve_update_branch(args) -> str:
return (getattr(args, "branch", None) or "main").strip() or "main"
def _widen_remote_branches(git_cmd: list) -> None:
"""Let the checkout resolve branches other than the one it was cloned on.
Installer checkouts come from ``git clone --depth 1 --branch main``, which
implies ``--single-branch`` and pins ``remote.origin.fetch`` to main. Our
scoped ``git fetch origin <branch>`` then updates FETCH_HEAD but never
creates ``origin/<branch>``, so ``--branch <other>`` reports the branch as
missing from a remote that plainly has it.
Restoring the wildcard costs no bandwidth — every fetch we issue names its
branch, and git only walks the full refspec on a bare ``git fetch``.
Best-effort: a failure here just leaves the checkout as it was.
"""
subprocess.run(
git_cmd + ["remote", "set-branches", "origin", "*"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True, encoding="utf-8", errors="replace",
)
def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
"""Implement ``hermes update --check``: fetch and report without installing.
@ -10797,6 +10818,7 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
== "true"
)
depth_args = ["--depth", "1"] if is_shallow else []
_widen_remote_branches(git_cmd)
if branch == "main":
print("→ Fetching from upstream...")
@ -12052,6 +12074,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
# minutes on a non-single-branch checkout. Fetch only what we update
# against.
branch = _resolve_update_branch(args)
_widen_remote_branches(git_cmd)
print("→ Fetching updates...")
fetch_result = subprocess.run(

View file

@ -852,6 +852,24 @@ def test_cmd_update_fetch_is_scoped_to_target_branch(monkeypatch, tmp_path):
assert ["git", "fetch", "origin"] not in recorded
def test_cmd_update_widens_remote_branches_before_fetching(monkeypatch, tmp_path):
"""Installer checkouts are single-branch, so a scoped `git fetch origin
<branch>` never creates origin/<branch> and `--branch <other>` fails the
checkout as missing. Widen the refspec first — free, since the fetch that
follows still names its branch."""
_setup_update_mocks(monkeypatch, tmp_path)
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
side_effect, recorded = _make_update_side_effect()
monkeypatch.setattr(hermes_main.subprocess, "run", side_effect)
hermes_main.cmd_update(SimpleNamespace())
widen = ["git", "remote", "set-branches", "origin", "*"]
assert widen in recorded
assert recorded.index(widen) < recorded.index(["git", "fetch", "origin", "main"])
# ---------------------------------------------------------------------------
# Fetch failure — friendly error messages
# ---------------------------------------------------------------------------