From 6cccc2ef7ecd7eb7204b0fe10bc339e8e2d63d99 Mon Sep 17 00:00:00 2001 From: SayHell0W0rld <852938468@qq.com> Date: Mon, 17 Aug 2026 21:16:08 +0800 Subject: [PATCH] fix(update): locate PortableGit under the shared root, not profile home Review feedback on #88136 (monerostar): a profile-scoped `hermes update` sets HERMES_HOME to /profiles/, but the Hermes-managed PortableGit tree lives under the SHARED root (/git/...). The locator checked get_hermes_home() only, so a broken trampoline during a profile-scoped update was not swapped and fell through to ZIP. Extract _portable_git_candidates() (shared root first, profile home as fallback) and add a regression test for the profile layout. --- hermes_cli/update_cmd.py | 35 ++++++++++++++++++----------- tests/hermes_cli/test_cmd_update.py | 21 +++++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index 4b3eea5b88..4421afdd22 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -37,7 +37,7 @@ from pathlib import Path from typing import Optional from hermes_cli.config import get_hermes_home -from hermes_constants import venv_python_path +from hermes_constants import get_default_hermes_root, venv_python_path logger = logging.getLogger(__name__) @@ -7381,6 +7381,26 @@ def _git_is_trampoline(git_cmd: list) -> bool: return "fork bomb" in output +def _portable_git_candidates() -> list: + """PortableGit candidate paths: shared root first, then profile home. + + The Hermes-managed PortableGit tree lives under the SHARED root + (``/git/...``), not the profile-scoped HERMES_HOME + (``/profiles/``), so a profile-scoped ``hermes update`` must + look there (monerostar review, #87876). The profile-home candidate is + kept as a fallback for custom layouts that place it there. + """ + candidates = [] + try: + for root in (get_default_hermes_root(), Path(get_hermes_home())): + candidates.append( + root / "git" / "mingw64" / "libexec" / "git-core" / "git.exe" + ) + except Exception: + pass + return candidates + + def _locate_real_git() -> Optional[Path]: """Find a real Git-for-Windows binary that is not a broken trampoline. @@ -7396,18 +7416,7 @@ def _locate_real_git() -> Optional[Path]: candidates = [ Path(r"C:\Program Files\Git\mingw64\libexec\git-core\git.exe"), Path(r"C:\Program Files (x86)\Git\mingw64\libexec\git-core\git.exe"), - ] - try: - candidates.append( - Path(get_hermes_home()) - / "git" - / "mingw64" - / "libexec" - / "git-core" - / "git.exe" - ) - except Exception: - pass + ] + _portable_git_candidates() for candidate in candidates: if not candidate.exists(): continue diff --git a/tests/hermes_cli/test_cmd_update.py b/tests/hermes_cli/test_cmd_update.py index 674c096b0c..117b39b371 100644 --- a/tests/hermes_cli/test_cmd_update.py +++ b/tests/hermes_cli/test_cmd_update.py @@ -1509,3 +1509,24 @@ class TestGitTrampolineSelfHeal: result = update_cmd._ensure_non_trampoline_git(git_cmd) assert result == git_cmd run.assert_not_called() + + def test_portable_git_candidates_check_shared_root_first(self, tmp_path, monkeypatch): + # Profile-scoped layout: HERMES_HOME = /profiles/foo, but the + # PortableGit tree lives under the SHARED root (monerostar review on + # #88136). The candidate list must check get_default_hermes_root() + # before the profile home. + from hermes_cli import update_cmd + + root = tmp_path / "root" + profile_home = root / "profiles" / "foo" + + monkeypatch.setattr(update_cmd, "get_default_hermes_root", lambda: root) + monkeypatch.setattr(update_cmd, "get_hermes_home", lambda: profile_home) + + candidates = update_cmd._portable_git_candidates() + assert candidates[0] == ( + root / "git" / "mingw64" / "libexec" / "git-core" / "git.exe" + ) + assert candidates[1] == ( + profile_home / "git" / "mingw64" / "libexec" / "git-core" / "git.exe" + )