fix(workspace-git): hide git subprocess console windows on Windows (#5692)

Add creationflags to _run_git() and _config_names_for_scope() so git
child processes don't accumulate hidden cmd.exe/conhost windows on Windows.

Uses a local module-level _windows_hide_flags() helper (mirrors the
api/updates.py pattern: CREATE_NO_WINDOW on win32, 0 elsewhere) rather than
importing from the OPTIONAL hermes_cli package — a standalone/agent-less
WebUI (and the repo's own CI runners, which don't pip-install hermes-agent)
must keep full workspace-git functionality. windows_hide_flags() is a safe
no-op (0) on non-Windows. Scoped to workspace git commands.

Adds mutation-checked tests asserting the kwarg is wired on both sites.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
Jinho Seok 2026-07-07 01:50:38 +00:00
parent 8fee7da3ee
commit f106fcd1f5
2 changed files with 75 additions and 0 deletions

View file

@ -12,6 +12,7 @@ import logging
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import re
@ -22,6 +23,20 @@ from typing import Iterable
logger = logging.getLogger(__name__)
def _windows_hide_flags() -> int:
"""Win32 ``creationflags`` that hide a short-lived console child's window
(``CREATE_NO_WINDOW``) without detaching it, so ``capture_output`` still
works. Returns ``0`` on non-Windows — the ``subprocess`` default, a genuine
no-op. Mirrors the ``api/updates.py`` pattern; kept local so workspace-git
never takes a hard dependency on the optional ``hermes_cli`` package (a
standalone/agent-less WebUI must keep full git functionality). See #5692.
"""
if sys.platform == "win32":
return getattr(subprocess, "CREATE_NO_WINDOW", 0)
return 0
from api.workspace import rmtree_anchored, safe_resolve_ws, unlink_anchored
@ -248,6 +263,7 @@ def _run_git(
text=True,
timeout=timeout,
env=run_env,
creationflags=_windows_hide_flags(),
)
except subprocess.TimeoutExpired as exc:
raise GitWorkspaceError("Git command timed out", "timeout") from exc
@ -290,6 +306,7 @@ def _config_names_for_scope(
capture_output=True,
timeout=GIT_TIMEOUT,
env=env,
creationflags=_windows_hide_flags(),
)
if result.returncode not in {0, 1}:
if ignore_unsupported:

View file

@ -2515,3 +2515,61 @@ def test_dirty_worktree_uses_filter_neutralization(tmp_path):
_dirty_worktree(ctx)
assert not marker.exists()
def test_run_git_passes_windows_hide_flags(monkeypatch, tmp_path):
"""_run_git must pass creationflags=_windows_hide_flags() so git child
processes don't accumulate visible console windows on Windows (#5692).
windows_hide_flags() is 0 on non-Windows, so this is a safe no-op there;
the test asserts the kwarg is wired regardless of platform."""
import api.workspace_git as wg
from api.workspace_git import _windows_hide_flags
repo = _init_repo(tmp_path / "repo")
(repo / "f.txt").write_text("x\n", encoding="utf-8")
_commit_all(repo)
captured = {}
real_run = subprocess.run
def fake_run(*args, **kwargs):
captured["creationflags"] = kwargs.get("creationflags", "MISSING")
return real_run(*args, **kwargs)
monkeypatch.setattr(wg.subprocess, "run", fake_run)
wg._run_git(repo, ["rev-parse", "HEAD"])
assert captured.get("creationflags") == _windows_hide_flags(), (
"_run_git must pass creationflags=_windows_hide_flags() to subprocess.run"
)
def test_config_names_for_scope_passes_windows_hide_flags(monkeypatch, tmp_path):
"""_config_names_for_scope must also pass creationflags=_windows_hide_flags()
(#5692) — the git-config probe spawns a console window on Windows too."""
import re as _re
import api.workspace_git as wg
from api.workspace_git import _windows_hide_flags
repo = _init_repo(tmp_path / "repo")
captured = {}
real_run = subprocess.run
def fake_run(*args, **kwargs):
captured["creationflags"] = kwargs.get("creationflags", "MISSING")
return real_run(*args, **kwargs)
monkeypatch.setattr(wg.subprocess, "run", fake_run)
wg._config_names_for_scope(
"--local",
repo,
{},
"filter\\..*",
_re.compile(r"^filter\."),
ignore_unsupported=True,
)
assert captured.get("creationflags") == _windows_hide_flags(), (
"_config_names_for_scope must pass creationflags=_windows_hide_flags()"
)