mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
feat(cli): add --branch flag to hermes update
`hermes update` has always hard-coded its target to `main`. Add --branch
so callers can update against a non-default channel while preserving every
existing behavior at the default:
- `hermes update` still pulls main (no behavior change)
- `hermes update --branch X` pulls origin/X, auto-stashing and switching
local HEAD to X first if needed
- `hermes update --check --branch X` reports behindness against
origin/X (and skips the upstream/X probe,
since forks don't have upstream copies of
their own feature branches)
- Branch absent locally → retry as `checkout -B X origin/X` (track)
- Branch absent everywhere → exit 1 with a clear error, after restoring
the user's prior stash so we don't strand
them in a weird state
The fork-upstream sync logic was already guarded on `branch == 'main'`,
so non-main updates correctly skip the upstream trampling without
further changes.
5 new tests cover: explicit --branch, default-to-main, switch-from-other,
track-from-origin, and the fail-cleanly case. Full test_cmd_update.py
suite (15 tests) passes on main.
This commit is contained in:
parent
5672772dab
commit
51689a4206
2 changed files with 235 additions and 27 deletions
|
|
@ -8045,8 +8045,13 @@ def _finalize_update_output(state):
|
|||
pass
|
||||
|
||||
|
||||
def _cmd_update_check():
|
||||
"""Implement ``hermes update --check``: fetch and report without installing."""
|
||||
def _cmd_update_check(branch: str = "main"):
|
||||
"""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.
|
||||
"""
|
||||
from hermes_cli.config import detect_install_method
|
||||
method = detect_install_method(PROJECT_ROOT)
|
||||
if method == "pip":
|
||||
|
|
@ -8072,16 +8077,34 @@ def _cmd_update_check():
|
|||
if sys.platform == "win32":
|
||||
git_cmd = ["git", "-c", "windows.appendAtomically=false"]
|
||||
|
||||
# Fetch both origin and upstream; prefer upstream as the canonical reference
|
||||
print("→ Fetching from upstream...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "upstream"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if fetch_result.returncode != 0:
|
||||
# Fallback to origin if upstream doesn't exist
|
||||
# Fetch both origin and upstream; prefer upstream as the canonical reference.
|
||||
# Note: upstream/<branch> may not exist for non-main branches (a fork's
|
||||
# bb/gui has no upstream counterpart), so when the caller picks a
|
||||
# non-default branch we skip the upstream probe and use origin directly.
|
||||
if branch == "main":
|
||||
print("→ Fetching from upstream...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "upstream"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if fetch_result.returncode != 0:
|
||||
# Fallback to origin if upstream doesn't exist
|
||||
print("→ Fetching from origin...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "origin"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
upstream_exists = False
|
||||
compare_branch = f"origin/{branch}"
|
||||
else:
|
||||
upstream_exists = True
|
||||
compare_branch = f"upstream/{branch}"
|
||||
else:
|
||||
# Non-default branch: compare against origin/<branch> directly.
|
||||
print("→ Fetching from origin...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "origin"],
|
||||
|
|
@ -8090,10 +8113,7 @@ def _cmd_update_check():
|
|||
text=True,
|
||||
)
|
||||
upstream_exists = False
|
||||
compare_branch = "origin/main"
|
||||
else:
|
||||
upstream_exists = True
|
||||
compare_branch = "upstream/main"
|
||||
compare_branch = f"origin/{branch}"
|
||||
|
||||
if fetch_result.returncode != 0:
|
||||
stderr = fetch_result.stderr.strip()
|
||||
|
|
@ -8325,7 +8345,10 @@ def cmd_update(args):
|
|||
return
|
||||
|
||||
if getattr(args, "check", False):
|
||||
_cmd_update_check()
|
||||
# --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)
|
||||
return
|
||||
|
||||
gateway_mode = getattr(args, "gateway", False)
|
||||
|
|
@ -8485,26 +8508,57 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
)
|
||||
current_branch = result.stdout.strip()
|
||||
|
||||
# Always update against main
|
||||
branch = "main"
|
||||
# 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"
|
||||
|
||||
# If user is on a non-main branch or detached HEAD, switch to main
|
||||
if current_branch != "main":
|
||||
# If user is on a different branch than the update target, switch
|
||||
# to the target. When the target is "main" this is the historical
|
||||
# "always update against main" behavior; for any other target it's
|
||||
# the same thing — get HEAD onto the requested branch first, then
|
||||
# fast-forward.
|
||||
if current_branch != branch:
|
||||
label = (
|
||||
"detached HEAD"
|
||||
if current_branch == "HEAD"
|
||||
else f"branch '{current_branch}'"
|
||||
)
|
||||
print(f" ⚠ Currently on {label} — switching to main for update...")
|
||||
print(f" ⚠ Currently on {label} — switching to {branch} for update...")
|
||||
# Stash before checkout so uncommitted work isn't lost
|
||||
auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)
|
||||
subprocess.run(
|
||||
git_cmd + ["checkout", "main"],
|
||||
checkout_result = subprocess.run(
|
||||
git_cmd + ["checkout", branch],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if checkout_result.returncode != 0:
|
||||
# Local checkout doesn't have this branch yet. Try to set
|
||||
# it up as a tracking branch of origin/<branch>. This is
|
||||
# the common case when the requested branch exists upstream
|
||||
# but was never checked out locally.
|
||||
track_result = subprocess.run(
|
||||
git_cmd + ["checkout", "-B", branch, f"origin/{branch}"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if track_result.returncode != 0:
|
||||
# Restore the user's prior branch + stash before bailing
|
||||
# so we don't leave them stranded in a weird state.
|
||||
if auto_stash_ref is not None:
|
||||
_restore_stashed_changes(
|
||||
git_cmd,
|
||||
PROJECT_ROOT,
|
||||
auto_stash_ref,
|
||||
prompt_user=False,
|
||||
input_fn=gw_input_fn,
|
||||
)
|
||||
print(f"✗ Branch '{branch}' does not exist locally or on origin.")
|
||||
if track_result.stderr.strip():
|
||||
print(f" {track_result.stderr.strip().splitlines()[0]}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)
|
||||
|
||||
|
|
@ -8535,7 +8589,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
prompt_user=prompt_for_restore,
|
||||
input_fn=gw_input_fn,
|
||||
)
|
||||
if current_branch not in {"main", "HEAD"}:
|
||||
if current_branch not in {branch, "HEAD"}:
|
||||
subprocess.run(
|
||||
git_cmd + ["checkout", current_branch],
|
||||
cwd=PROJECT_ROOT,
|
||||
|
|
@ -8597,7 +8651,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
if reset_result.stderr.strip():
|
||||
print(f" {reset_result.stderr.strip()}")
|
||||
print(
|
||||
" Try manually: git fetch origin && git reset --hard origin/main"
|
||||
f" Try manually: git fetch origin && git reset --hard origin/{branch}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -12835,6 +12889,17 @@ Examples:
|
|||
default=False,
|
||||
help="Assume yes for interactive prompts (config migration, stash restore). API-key entry is skipped; run 'hermes config migrate' separately for those.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--branch",
|
||||
default=None,
|
||||
metavar="NAME",
|
||||
help=(
|
||||
"Update against this branch instead of the default (main). "
|
||||
"If the local checkout is on a different branch, hermes will "
|
||||
"switch to the requested branch first (auto-stashing any "
|
||||
"uncommitted changes)."
|
||||
),
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue