diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 2ccfe808d4f..ce8862e2aaa 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -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 `` then updates FETCH_HEAD but never + creates ``origin/``, so ``--branch `` 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( diff --git a/tests/hermes_cli/test_update_autostash.py b/tests/hermes_cli/test_update_autostash.py index bd1638fcf65..96b46c15907 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -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 + ` never creates origin/ and `--branch ` 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 # ---------------------------------------------------------------------------