mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(update): don't count across shallow-clone boundary (bogus '12492 commits behind') (#50784)
* chore: re-trigger CI (workflows did not dispatch on prior head)
* fix(update): don't count across shallow-clone boundary (bogus '12492 commits behind')
Installer checkouts are shallow (git clone --depth 1). The CLI banner and
hermes update --check both did a plain git fetch (silently unshallowing the
repo) then git rev-list --count HEAD..origin/main, which counts across the
shallow boundary and prints a huge nonsense number like '12492 commits behind'.
Detect shallow up front, fetch with --depth 1 to preserve the boundary, and
compare tip SHAs instead of counting:
- banner _check_via_local_git: returns UPDATE_AVAILABLE_NO_COUNT when behind
(renders as 'update available') instead of the bogus count.
- _cmd_update_check: reports presence-only on shallow clones.
Full clones keep the exact count path unchanged. Mirrors the desktop fix in
apps/desktop/electron/main.cjs (commit 2950c6fa2).
This commit is contained in:
parent
2e779d11a0
commit
eecb5b9dd1
3 changed files with 163 additions and 5 deletions
|
|
@ -199,15 +199,43 @@ def _check_via_local_git(repo_dir: Path) -> Optional[int]:
|
|||
head_rev = _git_stdout(["rev-parse", "HEAD"], cwd=repo_dir)
|
||||
return _check_via_rev(head_rev) if head_rev else None
|
||||
|
||||
# Installer checkouts are shallow (`git clone --depth 1`). On a shallow
|
||||
# clone the history stops at a single commit, so a plain `git fetch` would
|
||||
# unshallow the repo (dragging in the whole history) and
|
||||
# `rev-list --count HEAD..origin/main` would report a huge bogus "behind"
|
||||
# number (e.g. "12492 commits behind"). Detect shallow up front: fetch with
|
||||
# --depth 1 to preserve the boundary and compare tip SHAs instead of
|
||||
# counting. Full clones (developers, Docker dev images) keep the exact
|
||||
# count path unchanged. Mirrors the desktop fix in apps/desktop/electron/main.cjs.
|
||||
shallow = _git_stdout(["rev-parse", "--is-shallow-repository"], cwd=repo_dir)
|
||||
is_shallow = shallow == "true"
|
||||
|
||||
try:
|
||||
fetch_args = ["git", "fetch", "origin"]
|
||||
if is_shallow:
|
||||
fetch_args += ["--depth", "1"]
|
||||
fetch_args.append("--quiet")
|
||||
subprocess.run(
|
||||
["git", "fetch", "origin", "--quiet"],
|
||||
fetch_args,
|
||||
capture_output=True, timeout=10,
|
||||
cwd=str(repo_dir),
|
||||
)
|
||||
except Exception:
|
||||
pass # Offline or timeout — use stale refs, that's fine
|
||||
|
||||
if is_shallow:
|
||||
# No history to count across the shallow boundary. `origin/main` may not
|
||||
# be a tracking ref in a `clone --depth 1`, so prefer FETCH_HEAD (just
|
||||
# updated by the fetch above) and fall back to origin/main.
|
||||
head_rev = _git_stdout(["rev-parse", "HEAD"], cwd=repo_dir)
|
||||
target_rev = (
|
||||
_git_stdout(["rev-parse", "FETCH_HEAD"], cwd=repo_dir)
|
||||
or _git_stdout(["rev-parse", "origin/main"], cwd=repo_dir)
|
||||
)
|
||||
if not head_rev or not target_rev:
|
||||
return None
|
||||
return 0 if head_rev == target_rev else UPDATE_AVAILABLE_NO_COUNT
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-list", "--count", "HEAD..origin/main"],
|
||||
|
|
|
|||
|
|
@ -8040,10 +8040,26 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
|
|||
# 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.
|
||||
# Installer checkouts are shallow (`git clone --depth 1`). A plain
|
||||
# `git fetch` would unshallow the repo (dragging in the whole history —
|
||||
# the exact cost the shallow clone avoided) and the rev-list count below
|
||||
# would then report a huge bogus "behind" number. Detect shallow up front:
|
||||
# fetch with --depth 1 to preserve the boundary and report presence-only.
|
||||
is_shallow = (
|
||||
subprocess.run(
|
||||
git_cmd + ["rev-parse", "--is-shallow-repository"],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
== "true"
|
||||
)
|
||||
depth_args = ["--depth", "1"] if is_shallow else []
|
||||
|
||||
if branch == "main":
|
||||
print("→ Fetching from upstream...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "upstream", branch],
|
||||
git_cmd + ["fetch"] + depth_args + ["upstream", branch],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
|
@ -8052,7 +8068,7 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
|
|||
# Fallback to origin if upstream doesn't exist
|
||||
print("→ Fetching from origin...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "origin", branch],
|
||||
git_cmd + ["fetch"] + depth_args + ["origin", branch],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
|
@ -8066,7 +8082,7 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
|
|||
# Non-default branch: compare against origin/<branch> directly.
|
||||
print("→ Fetching from origin...")
|
||||
fetch_result = subprocess.run(
|
||||
git_cmd + ["fetch", "origin", branch],
|
||||
git_cmd + ["fetch"] + depth_args + ["origin", branch],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
|
@ -8100,6 +8116,26 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
|
|||
print(f"✗ Branch '{branch}' not found on {compare_branch.split('/', 1)[0]}.")
|
||||
sys.exit(1)
|
||||
|
||||
if is_shallow:
|
||||
# No history to count across the shallow boundary. Compare tip SHAs and
|
||||
# report presence-only (mirrors the banner's _check_via_local_git).
|
||||
head_sha = subprocess.run(
|
||||
git_cmd + ["rev-parse", "HEAD"],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
target_sha = subprocess.run(
|
||||
git_cmd + ["rev-parse", compare_branch],
|
||||
cwd=PROJECT_ROOT, capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
if head_sha and target_sha and head_sha == target_sha:
|
||||
print("✓ Already up to date.")
|
||||
else:
|
||||
print(f"⚕ Update available (behind {compare_branch}).")
|
||||
from hermes_cli.config import recommended_update_command
|
||||
|
||||
print(f" Run '{recommended_update_command()}' to install.")
|
||||
return
|
||||
|
||||
rev_result = subprocess.run(
|
||||
git_cmd + ["rev-list", f"HEAD..{compare_branch}", "--count"],
|
||||
cwd=PROJECT_ROOT,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ def test_check_for_updates_expired_cache(tmp_path, monkeypatch):
|
|||
result = check_for_updates()
|
||||
|
||||
assert result == 5
|
||||
assert mock_run.call_count == 3 # origin probe + git fetch + git rev-list
|
||||
# origin probe + is-shallow probe + git fetch + git rev-list
|
||||
assert mock_run.call_count == 4
|
||||
|
||||
|
||||
def test_check_for_updates_official_ssh_origin_uses_https_probe(tmp_path):
|
||||
|
|
@ -128,6 +129,99 @@ def test_check_for_updates_official_ssh_origin_uses_https_probe(tmp_path):
|
|||
assert ["git", "fetch", "origin", "--quiet"] not in calls
|
||||
|
||||
|
||||
def test_check_via_local_git_shallow_clone_behind_reports_no_count(tmp_path):
|
||||
"""Shallow installer clones must report presence-only, never a bogus count.
|
||||
|
||||
On a ``git clone --depth 1`` checkout the history stops at one commit, so
|
||||
counting ``HEAD..origin/main`` across the shallow boundary yields a huge
|
||||
nonsense number (the "12492 commits behind" banner). The shallow path must
|
||||
compare tip SHAs and return UPDATE_AVAILABLE_NO_COUNT instead, and must
|
||||
never run ``git rev-list --count``.
|
||||
"""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
repo_dir = tmp_path / "hermes-agent"
|
||||
repo_dir.mkdir()
|
||||
(repo_dir / ".git").mkdir()
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
calls.append(cmd)
|
||||
if cmd == ["git", "remote", "get-url", "origin"]:
|
||||
return MagicMock(returncode=0, stdout="https://github.com/NousResearch/hermes-agent.git\n")
|
||||
if cmd == ["git", "rev-parse", "--is-shallow-repository"]:
|
||||
return MagicMock(returncode=0, stdout="true\n")
|
||||
if cmd[:2] == ["git", "fetch"]:
|
||||
return MagicMock(returncode=0, stdout="")
|
||||
if cmd == ["git", "rev-parse", "HEAD"]:
|
||||
return MagicMock(returncode=0, stdout="local-sha\n")
|
||||
if cmd == ["git", "rev-parse", "FETCH_HEAD"]:
|
||||
return MagicMock(returncode=0, stdout="upstream-sha\n")
|
||||
if cmd[:3] == ["git", "rev-list", "--count"]:
|
||||
raise AssertionError("shallow path must not count across the boundary")
|
||||
raise AssertionError(f"unexpected git command: {cmd!r}")
|
||||
|
||||
with patch("hermes_cli.banner.subprocess.run", side_effect=fake_run):
|
||||
result = banner._check_via_local_git(repo_dir)
|
||||
|
||||
assert result == banner.UPDATE_AVAILABLE_NO_COUNT
|
||||
# The shallow fetch must preserve the boundary (--depth 1), not unshallow.
|
||||
assert ["git", "fetch", "origin", "--depth", "1", "--quiet"] in calls
|
||||
|
||||
|
||||
def test_check_via_local_git_shallow_clone_up_to_date(tmp_path):
|
||||
"""Shallow clone whose tip matches upstream reports up-to-date (0)."""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
repo_dir = tmp_path / "hermes-agent"
|
||||
repo_dir.mkdir()
|
||||
(repo_dir / ".git").mkdir()
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd == ["git", "remote", "get-url", "origin"]:
|
||||
return MagicMock(returncode=0, stdout="https://github.com/NousResearch/hermes-agent.git\n")
|
||||
if cmd == ["git", "rev-parse", "--is-shallow-repository"]:
|
||||
return MagicMock(returncode=0, stdout="true\n")
|
||||
if cmd[:2] == ["git", "fetch"]:
|
||||
return MagicMock(returncode=0, stdout="")
|
||||
if cmd == ["git", "rev-parse", "HEAD"]:
|
||||
return MagicMock(returncode=0, stdout="same-sha\n")
|
||||
if cmd == ["git", "rev-parse", "FETCH_HEAD"]:
|
||||
return MagicMock(returncode=0, stdout="same-sha\n")
|
||||
raise AssertionError(f"unexpected git command: {cmd!r}")
|
||||
|
||||
with patch("hermes_cli.banner.subprocess.run", side_effect=fake_run):
|
||||
result = banner._check_via_local_git(repo_dir)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
def test_check_via_local_git_full_clone_keeps_exact_count(tmp_path):
|
||||
"""Full (non-shallow) clones keep the exact rev-list count path."""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
repo_dir = tmp_path / "hermes-agent"
|
||||
repo_dir.mkdir()
|
||||
(repo_dir / ".git").mkdir()
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
if cmd == ["git", "remote", "get-url", "origin"]:
|
||||
return MagicMock(returncode=0, stdout="https://github.com/NousResearch/hermes-agent.git\n")
|
||||
if cmd == ["git", "rev-parse", "--is-shallow-repository"]:
|
||||
return MagicMock(returncode=0, stdout="false\n")
|
||||
if cmd[:2] == ["git", "fetch"]:
|
||||
return MagicMock(returncode=0, stdout="")
|
||||
if cmd[:3] == ["git", "rev-list", "--count"]:
|
||||
return MagicMock(returncode=0, stdout="7\n")
|
||||
raise AssertionError(f"unexpected git command: {cmd!r}")
|
||||
|
||||
with patch("hermes_cli.banner.subprocess.run", side_effect=fake_run):
|
||||
result = banner._check_via_local_git(repo_dir)
|
||||
|
||||
assert result == 7
|
||||
|
||||
|
||||
def test_check_for_updates_no_git_dir(tmp_path, monkeypatch):
|
||||
"""Falls back to PyPI check when .git directory doesn't exist anywhere."""
|
||||
import hermes_cli.banner as banner
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue