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" + )