diff --git a/hermes_cli/gitlock.py b/hermes_cli/gitlock.py index 4aa5ae91ad..d7ee1a510b 100644 --- a/hermes_cli/gitlock.py +++ b/hermes_cli/gitlock.py @@ -391,19 +391,18 @@ def prune_stale_shallow_grafts(repo_root: Path) -> int: def fetch_full_commit_graph(repo_root: Path, **run_kwargs) -> bool: - """Turn a shallow checkout into a treeless partial clone of ``origin``. + """Refresh release tags and fill shallow history before publishing identity. - Version identity is the nearest reachable release tag plus the commit count since - it, and a shallow boundary hides both. Only commits (and the tags that follow - them) are fetched; trees and blobs stay on demand, so this costs a fraction of a - full unshallow. Returns False when the checkout was not shallow; raises - ``subprocess.CalledProcessError`` / ``TimeoutExpired`` when the fetch fails. + A full commit graph does not imply current tags, especially after a --no-tags + clone. Fetch version tags explicitly without fetching every remote branch or + replacing existing tags. Trees and blobs stay on demand. Returns whether the + checkout was unshallowed; fetch failures raise subprocess errors. """ - if _shallow_file_path(repo_root) is None: - return False + shallow = _shallow_file_path(repo_root) is not None subprocess.run( - ["git", "fetch", "--quiet", "--unshallow", "--filter=tree:0", "origin"], + ["git", "fetch", "--quiet", *(["--unshallow"] if shallow else []), + "--filter=tree:0", "--no-tags", "origin", "refs/tags/v*:refs/tags/v*"], cwd=str(repo_root), check=True, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=900, **run_kwargs, ) - return True + return shallow diff --git a/hermes_cli/update_cmd_maint.py b/hermes_cli/update_cmd_maint.py index d575b7d915..2a50327528 100644 --- a/hermes_cli/update_cmd_maint.py +++ b/hermes_cli/update_cmd_maint.py @@ -990,9 +990,8 @@ def _run_post_update_maintenance( with _best_effort('Post-update state.db integrity check failed: %s'): _verify_and_restore_state_dbs_post_update() - # Pre-PM installers cloned --depth 1, which hides the release tag identity is - # derived from. Fetch the commit graph before the completion line and the - # install stamp read that identity. + # Both shallow history and missing tags can hide the release identity. + # Refresh them before the completion line and install stamp read it. try: from hermes_cli.gitlock import fetch_full_commit_graph from hermes_cli.update_cmd import _no_prompt_git_kwargs @@ -1000,7 +999,7 @@ def _run_post_update_maintenance( print(" ✓ Fetched release history (commits only) for version identity") except (OSError, subprocess.SubprocessError) as exc: detail = (getattr(exc, "stderr", None) or str(exc)).strip().splitlines()[-1:] or [type(exc).__name__] - print(f" ⚠ Could not fetch release history ({detail[0]}); the version shows as git. until the next update") + print(f" ⚠ Could not refresh release history ({detail[0]}); the version label may be stale or unknown until the next update") # Seed the model-catalog cache from the checkout instead of a bot-gated, flaky fetch. with _best_effort('Model catalog seed during update failed: %s'): diff --git a/tests/hermes_cli/test_source_completion_stamp.py b/tests/hermes_cli/test_source_completion_stamp.py index d17099c778..cdf82639fb 100644 --- a/tests/hermes_cli/test_source_completion_stamp.py +++ b/tests/hermes_cli/test_source_completion_stamp.py @@ -108,3 +108,41 @@ def test_shallow_checkout_publishes_its_release_after_fetching_the_commit_graph( assert stamp is not None assert (stamp["baseVersion"], stamp["distance"]) == ("0.21.4", 3) assert not fetch_full_commit_graph(checkout) + + +def test_full_checkout_refreshes_release_tags_before_publishing_identity(tmp_path): + from hermes_cli.gitlock import fetch_full_commit_graph + + server = _repo(tmp_path) + env = {"HOME": str(tmp_path), "PATH": os.environ["PATH"]} + + def git(root: Path, *args: str) -> str: + return subprocess.run( + ["git", *args], cwd=root, env=env, check=True, capture_output=True, text=True, + ).stdout.strip() + + git(server, "config", "uploadpack.allowFilter", "true") + git(server, "tag", "-d", "v0.21.4") + versions = (("v2026.9.7", "0.21.1"), ("v2026.9.24", "0.21.5")) + for tag, version in versions: + (server / "pyproject.toml").write_text(f'[project]\nversion = "{version}"\n', encoding="utf-8") + git(server, "add", "pyproject.toml") + git(server, "commit", "-qm", "release") + git(server, "tag", tag) + git(server, "commit", "-q", "--allow-empty", "-m", "after release") + checkout = tmp_path / "checkout" + git(server, "clone", "-q", "--no-tags", server.as_uri(), str(checkout)) + old_tag, old_version = versions[0] + git(checkout, "fetch", "-q", "origin", f"refs/tags/{old_tag}:refs/tags/{old_tag}") + commit = git(checkout, "rev-parse", "HEAD") + assert git(checkout, "rev-parse", "--is-shallow-repository") == "false" + before = write_source_stamp(checkout) + assert before is not None + assert before["baseVersion"] == old_version + + fetch_full_commit_graph(checkout) + stamp = write_source_stamp(checkout) + + assert stamp is not None + assert (stamp["baseVersion"], stamp["distance"]) == (versions[1][1], 1) + assert stamp["commit"] == commit == git(checkout, "rev-parse", "HEAD")