fix(update): expose PM's git only for git checkouts, and record the installer's copy

A git-less Windows ZIP install has no .git and never runs git, yet
_prepare_git_command called expose_pm_git() before it checked
use_zip_update: offline or with no pin, pm.ensure raised and aborted an
update that never needed git. expose_pm_git now takes the project root
and does nothing unless it is a git checkout (both callers).

When install.ps1 runs as one process, the git it staged into PM's store
is on the inherited PATH, so expose_pm_git returned early and PM's facts
never recorded it; plain hermes processes (plugin git installs, doctor,
version info) had no git until the first `hermes update`. A git found
under PM's store is now treated as that unrecorded copy and ensured, so
the source completion writes the git fact at install time.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
This commit is contained in:
teknium1
2026-09-27 01:04:18 -07:00
committed by Teknium
parent e5ccf4a26c
commit 2270d3511d
4 changed files with 43 additions and 22 deletions

View File

@@ -13,6 +13,7 @@ import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Mapping, NoReturn, Sequence
__all__ = [
@@ -372,22 +373,26 @@ def selected_git_env(base: Mapping[str, str] | None = None) -> dict[str, str]:
return env
def expose_pm_git() -> None:
"""Put PM's git on this process's PATH when Windows has no git of its own.
def expose_pm_git(project_root: Path) -> None:
"""Put PM's git on PATH, and in PM's facts, for a Windows git checkout.
On a machine without git, install.ps1 stages the pinned Git for Windows for
its own process only; PM's facts never record it. Every later bare ``git``
(``hermes update``, the source-completion stamp) then died with
``[WinError 2]``. Callers are explicit user actions (like
``ensure_tools_for_sync``), so acquire PM's git outright; children inherit
the PATH. A machine with a working git is untouched. Raises what
``pm.ensure`` raises.
install.ps1 stages the pinned Git for Windows into PM's store for its own
process only, and PM's facts never record it, so every later bare ``git``
(``hermes update``, the source-completion stamp, plugin installs, doctor)
died with ``[WinError 2]``. A git found under PM's store is that unrecorded
copy inherited from the installer, so it is recorded too. Callers are
explicit user actions (like ``ensure_tools_for_sync``), so acquire PM's git
outright; children inherit the PATH. The machine's own git, and a git-less
ZIP install that never runs git, are untouched. Raises what ``pm.ensure``
raises.
"""
if sys.platform != "win32":
if sys.platform != "win32" or not (Path(project_root) / ".git").exists():
return
from hermes_platform.resolver import locate_command
from pm.paths import store_root
if locate_command("git").found:
found = locate_command("git").command
if found and not Path(found[0]).resolve().is_relative_to(store_root()):
return
from pm import ensure

View File

@@ -52,7 +52,7 @@ def complete_source_checkout(
# The builds, the release-history refresh and the install stamp all run
# git; a fresh Windows machine has only PM's.
expose_pm_git()
expose_pm_git(root)
except Exception as exc: # noqa: BLE001 — git-less steps below still complete
print(f"⚠ Could not provide git for the source completion: {exc}", file=sys.stderr)
publish_launchers(root)

View File

@@ -1106,7 +1106,7 @@ def _prepare_git_command() -> tuple[bool, list, bool]:
from hermes_cli._subprocess_compat import expose_pm_git
expose_pm_git()
expose_pm_git(_m().PROJECT_ROOT)
git_cmd = _base_git_cmd()
if sys.platform == "win32" and git_dir.exists():
_git_run(git_cmd, ["config", "windows.appendAtomically", "false"])

View File

@@ -1,7 +1,8 @@
"""Windows machines whose only git is the one PM provides (install.ps1 fresh installs)."""
"""Windows installs whose git is PM's: install.ps1 stages it, PM's facts must record it."""
import os
import pm
import pm.paths
import pytest
from hermes_cli import _subprocess_compat as compat
@@ -9,12 +10,21 @@ from pm.package import Runner
@pytest.fixture
def windows(monkeypatch):
def windows(monkeypatch, tmp_path):
store = tmp_path / "tools"
staged_git = store / "git-2.53.0+3-win32-x64" / "cmd" / "git.exe"
staged_git.parent.mkdir(parents=True)
staged_git.touch()
(tmp_path / "checkout" / ".git").mkdir(parents=True)
monkeypatch.setattr(compat.sys, "platform", "win32")
monkeypatch.setattr(pm.paths, "store_root", lambda: store)
monkeypatch.setenv("PATH", r"C:\Windows\System32")
return tmp_path, staged_git
def test_pm_git_goes_on_path_when_windows_has_none(windows, monkeypatch):
@pytest.mark.parametrize("git_on_path", ["none", "installer-staged"])
def test_pm_git_is_acquired_and_recorded_when_windows_has_no_git_of_its_own(windows, monkeypatch, git_on_path):
root, staged_git = windows
calls = []
store_path = r"C:\store\git-2.53.0+3-win32-x64\cmd;C:\store\git-2.53.0+3-win32-x64\usr\bin;C:\Windows\System32"
@@ -22,19 +32,25 @@ def test_pm_git_goes_on_path_when_windows_has_none(windows, monkeypatch):
calls.append((name, kwargs))
return Runner(name, {"Path": store_path})
monkeypatch.setattr(compat.shutil, "which", lambda name, *a, **k: None)
found = str(staged_git) if git_on_path == "installer-staged" else None
monkeypatch.setattr(compat.shutil, "which", lambda name, *a, **k: found)
monkeypatch.setattr(pm, "ensure", ensure)
compat.expose_pm_git()
compat.expose_pm_git(root / "checkout")
assert calls == [("git", {"explicit": True})]
assert os.environ["PATH"] == store_path
def test_a_working_windows_git_is_left_alone(windows, monkeypatch):
monkeypatch.setattr(compat.shutil, "which", lambda name, *a, **k: r"C:\Program Files\Git\cmd\git.exe")
monkeypatch.setattr(pm, "ensure", lambda *a, **k: pytest.fail("acquired PM git over a working one"))
@pytest.mark.parametrize("install", ["own-git", "git-less-zip"])
def test_own_git_and_git_less_zip_installs_are_left_alone(windows, monkeypatch, install):
root, _ = windows
own_git = str(root / "Git" / "cmd" / "git.exe") if install == "own-git" else None
if install == "git-less-zip":
(root / "checkout" / ".git").rmdir()
monkeypatch.setattr(compat.shutil, "which", lambda name, *a, **k: own_git)
monkeypatch.setattr(pm, "ensure", lambda *a, **k: pytest.fail("acquired PM git"))
compat.expose_pm_git()
compat.expose_pm_git(root / "checkout")
assert os.environ["PATH"] == r"C:\Windows\System32"