fix(cli): plug silent-divergence holes in --branch flag

Three follow-up fixes — all the same shape: silently doing the wrong
thing instead of either honoring --branch or refusing.

1) --check --branch <missing> raised CalledProcessError from
   'git rev-list ... --count' (check=True) when the branch didn't
   exist on origin. 'git fetch origin' succeeds without a refspec
   (it just fetches what's there), so the bad-branch case wasn't
   caught at the fetch step. Now verify the compare ref with
   'git rev-parse --verify --quiet' before rev-list and emit a
   friendly error.

2) _update_via_zip (Windows fallback for broken git file I/O)
   hard-coded branch = 'main', so on the ZIP path --branch=foo
   silently downloaded main.zip and told the user it worked. Refuse
   in that case instead — silently lying about which branch got
   installed is exactly what --branch was added to prevent.

3) _cmd_update_check PyPI path returned before looking at branch,
   so PyPI users running 'hermes update --check --branch=x' got a
   generic PyPI version check with no indication --branch was
   dropped. Now prints a one-line warning when --branch was explicit
   and non-main.

Also pull the '(getattr(args, branch, None) or main).strip() or main'
expression into _resolve_update_branch(args) — three callsites agree
on the same parsing.

Tests: 5 new tests for the --check + --branch matrix (named branch,
missing branch, default-main upstream-first, PyPI warning) and the
ZIP refusal. test_cmd_update.py is 20/20 green, broader hermes_cli/
suite (4952 tests) unchanged.
This commit is contained in:
emozilla 2026-05-21 02:14:08 -04:00
parent 51689a4206
commit d5b73937db
2 changed files with 224 additions and 5 deletions

View file

@ -6682,7 +6682,25 @@ def _update_via_zip(args):
import zipfile
from urllib.request import urlretrieve
branch = "main"
# The ZIP fallback exists for Windows git-file-I/O breakage. It pulls a
# static archive from GitHub, which is fine for the default "main"
# channel but would silently ignore --branch and update from main even
# if the user asked for something else — exactly the silent-divergence
# bug --branch was added to prevent. Refuse to proceed in that case
# rather than lie.
branch = _resolve_update_branch(args)
if branch != "main":
print(
f"✗ --branch={branch} is not supported on the Windows ZIP-fallback "
"update path."
)
print(
" This path runs when git file I/O is broken on the system. "
"Either resolve the git-side breakage (typically an antivirus "
"or NTFS filter holding files open) and rerun `hermes update "
f"--branch {branch}`, or update against main with `hermes update`."
)
sys.exit(1)
zip_url = (
f"https://github.com/NousResearch/hermes-agent/archive/refs/heads/{branch}.zip"
)
@ -8045,18 +8063,36 @@ def _finalize_update_output(state):
pass
def _cmd_update_check(branch: str = "main"):
def _resolve_update_branch(args) -> str:
"""Normalize ``args.branch`` into a non-empty branch name.
Centralizes the "default to main, accept --branch override, treat empty
or whitespace-only values as the default" parsing so every consumer of
``--branch`` (check path, git-update path, ZIP-fallback path) agrees on
the same answer.
"""
return (getattr(args, "branch", None) or "main").strip() or "main"
def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
"""Implement ``hermes update --check``: fetch and report without installing.
``branch`` selects which branch the check compares against. Default is
"main"; callers can pass another branch to ask "are there new commits
on origin/<branch>?" without performing the update.
``branch_explicit`` is True iff the caller passed --branch on the CLI.
PyPI installs can't honor non-default branches, so when this is True
on a PyPI install we surface a one-line notice instead of silently
dropping the flag.
"""
from hermes_cli.config import detect_install_method
method = detect_install_method(PROJECT_ROOT)
if method == "pip":
from hermes_cli.config import recommended_update_command
from hermes_cli.banner import check_via_pypi
if branch_explicit and branch != "main":
print(f"⚠ --branch is ignored for PyPI installs (would have checked '{branch}').")
result = check_via_pypi()
if result is None:
print("✗ Could not reach PyPI to check for updates.")
@ -8127,6 +8163,20 @@ def _cmd_update_check(branch: str = "main"):
print(f" {stderr.splitlines()[0]}")
sys.exit(1)
# Verify the compare ref actually exists before asking rev-list about it.
# Without this, `git rev-list HEAD..origin/<bogus> --count` exits 128 and
# (with check=True) raises CalledProcessError, surfacing a Python
# traceback. Friendlier to detect-and-report.
verify_result = subprocess.run(
git_cmd + ["rev-parse", "--verify", "--quiet", compare_branch],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
)
if verify_result.returncode != 0:
print(f"✗ Branch '{branch}' not found on {compare_branch.split('/', 1)[0]}.")
sys.exit(1)
rev_result = subprocess.run(
git_cmd + ["rev-list", f"HEAD..{compare_branch}", "--count"],
cwd=PROJECT_ROOT,
@ -8347,8 +8397,11 @@ def cmd_update(args):
if getattr(args, "check", False):
# --check honors --branch so the "any new commits?" answer matches
# what a subsequent `hermes update --branch=<x>` would actually pull.
branch = (getattr(args, "branch", None) or "main").strip() or "main"
_cmd_update_check(branch=branch)
branch = _resolve_update_branch(args)
_cmd_update_check(
branch=branch,
branch_explicit=bool(getattr(args, "branch", None)),
)
return
gateway_mode = getattr(args, "gateway", False)
@ -8511,7 +8564,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
# Determine the target branch. Default is "main" (the long-standing
# CLI behavior); --branch overrides for callers that want to update
# against a non-default channel.
branch = (getattr(args, "branch", None) or "main").strip() or "main"
branch = _resolve_update_branch(args)
# If user is on a different branch than the update target, switch
# to the target. When the target is "main" this is the historical