From a51100c308ca6d951a7db7a441101de74f1fe226 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 12 Aug 2026 19:00:12 +0800 Subject: [PATCH] emrg: resolve git exe path once (install-info -> bundled -> PATH fallback) --- emrg/server/git_utils.py | 8 +++----- emrg/server/scheduler.py | 29 +++++++++++++++++------------ tests/test_scheduler.py | 14 +++++++++----- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/emrg/server/git_utils.py b/emrg/server/git_utils.py index cf8aca7..e6df14f 100644 --- a/emrg/server/git_utils.py +++ b/emrg/server/git_utils.py @@ -133,11 +133,9 @@ def _detect_git_remote(cwd: str) -> str: Returns '' if detection fails. """ try: - result = subprocess.run( - ["git", "remote", "get-url", "origin"], - cwd=cwd, capture_output=True, text=True, encoding="utf-8", timeout=5, - **win32_no_window_kwargs(), - ) + # 用解析后的 git(install-info → bundled → PATH 回退,见 git_cmd), + # 防 daemon 启动环境无 PATH git 时误判 "not a git repo"(2026-08-12 事故)。 + result = git_cmd("remote", "get-url", "origin", cwd=cwd, timeout=5) if result.returncode == 0: url = result.stdout.strip() # Extract owner/repo from various URL formats: diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index a7fe98c..4f9c061 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -270,6 +270,11 @@ def __init__( self._repo_configured = project_name == "emrg" self._session_id = f"emrg-evolution-{name}" self._source_dir = path or name + # 解析一次 git 可执行路径(install-info.json → bundled → PATH 回退)。 + # 2026-08-12 事故:daemon 从无 PATH git 的环境重启后,裸 `git` 调用 + # FileNotFoundError → _is_usable_git_repo() 误判 "not a git repo" → + # 演化周期全部跳过。此后所有 git 调用走 resolve_git_gh() 的确定性解析。 + self._git_exe = resolve_git_gh()[0] or "git" # One-shot https-origin probe per handler lifetime (see # _ensure_origin_reachable) — avoids re-probing every cycle. self._origin_probed = False @@ -278,7 +283,7 @@ def _get_git_head(self) -> str | None: """Return current git HEAD hash, or None if not a git repo.""" try: result = subprocess.run( - ["git", "rev-parse", "HEAD"], + [self._git_exe, "rev-parse", "HEAD"], cwd=self._source_dir, capture_output=True, text=True, @@ -317,7 +322,7 @@ def _is_usable_git_repo(self, path: str) -> bool: return False try: result = subprocess.run( - ["git", "rev-parse", "--is-inside-work-tree"], + [self._git_exe, "rev-parse", "--is-inside-work-tree"], cwd=path, capture_output=True, text=True, @@ -339,7 +344,7 @@ def _ensure_git_identity(self, repo_dir: Path) -> None: try: for key, default in (("user.name", name), ("user.email", email)): result = subprocess.run( - ["git", "config", key], + [self._git_exe, "config", key], cwd=repo_dir, capture_output=True, text=True, @@ -350,7 +355,7 @@ def _ensure_git_identity(self, repo_dir: Path) -> None: ) if not result.stdout.strip(): subprocess.run( - ["git", "config", key, default], + [self._git_exe, "config", key, default], cwd=repo_dir, capture_output=True, timeout=5, @@ -381,7 +386,7 @@ def _align_to_installed_version(self, repo_dir: Path) -> None: return try: result = subprocess.run( - ["git", "tag", "-l", tag], + [self._git_exe, "tag", "-l", tag], cwd=repo_dir, capture_output=True, text=True, @@ -392,7 +397,7 @@ def _align_to_installed_version(self, repo_dir: Path) -> None: ) if result.returncode == 0 and tag in result.stdout.split(): subprocess.run( - ["git", "checkout", "-B", "master", tag], + [self._git_exe, "checkout", "-B", "master", tag], cwd=repo_dir, capture_output=True, text=True, @@ -507,7 +512,7 @@ def _clone_workspace(self, repo_url: str, target: Path) -> None: block https git transport (observed on the packaged host). Other failures (auth / 404 / repo-specific) propagate unchanged. """ - cmd = ["git", "-c", "http.connectTimeout=10", "clone", repo_url, str(target)] + cmd = [self._git_exe, "-c", "http.connectTimeout=10", "clone", repo_url, str(target)] reason = "" try: subprocess.run( @@ -527,7 +532,7 @@ def _clone_workspace(self, repo_url: str, target: Path) -> None: self.name, reason, ) subprocess.run( - ["git", "clone", ssh_url, str(target)], + [self._git_exe, "clone", ssh_url, str(target)], capture_output=True, text=True, encoding="utf-8", timeout=120, check=True, env=no_prompt_env(), **win32_no_window_kwargs(), @@ -553,7 +558,7 @@ def _ensure_origin_reachable(self) -> None: if not ssh_url: return # not a github.com https origin — nothing to switch result = subprocess.run( - ["git", "-c", "http.connectTimeout=4", "ls-remote", origin, "HEAD"], + [self._git_exe, "-c", "http.connectTimeout=4", "ls-remote", origin, "HEAD"], cwd=self._source_dir, capture_output=True, text=True, @@ -567,7 +572,7 @@ def _ensure_origin_reachable(self) -> None: if not is_git_connection_error(result.stderr): return # auth/404 etc — switching would not help switch = subprocess.run( - ["git", "remote", "set-url", "origin", ssh_url], + [self._git_exe, "remote", "set-url", "origin", ssh_url], cwd=self._source_dir, capture_output=True, text=True, @@ -731,7 +736,7 @@ def _remote_advanced(self) -> bool: if not local: return False result = subprocess.run( - ["git", "-c", "http.connectTimeout=4", "ls-remote", "origin", "master"], + [self._git_exe, "-c", "http.connectTimeout=4", "ls-remote", "origin", "master"], cwd=self._source_dir, capture_output=True, text=True, @@ -745,7 +750,7 @@ def _remote_advanced(self) -> bool: ssh_url = https_to_ssh_url(git_origin_url(self._source_dir)) if ssh_url and is_git_connection_error(result.stderr): result = subprocess.run( - ["git", "ls-remote", ssh_url, "master"], + [self._git_exe, "ls-remote", ssh_url, "master"], cwd=self._source_dir, capture_output=True, text=True, diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 977856c..97748ad 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -633,7 +633,11 @@ def _norm(cmd): def __call__(self, cmd, *args, **kwargs): self.calls.append((list(cmd), kwargs.get("cwd"))) cwd = kwargs.get("cwd") or "" - if cmd[0] == "git": + # cmd[0] 可能是字面 "git"(dev 环境)或 resolve_git_gh() 解析出的 + # 绝对路径(bundled git,2026-08-12 workspace-not-ready 事故修复后)—— + # 统一按 basename 判断,避免测试在两种环境下行为不一致。 + cmd_head = Path(cmd[0]).name + if cmd_head in ("git", "git.exe"): sub = self._norm(cmd) if sub and sub[0] == "rev-parse": if "--is-inside-work-tree" in sub: @@ -971,7 +975,7 @@ def test_ensure_origin_reachable_switches_to_ssh_when_https_blocked(tmp_path): set_url_calls = [ c for c in fake.calls - if c[0][0] == "git" and c[0][1] == "remote" and c[0][2] == "set-url" + if Path(c[0][0]).name in ("git", "git.exe") and c[0][1] == "remote" and c[0][2] == "set-url" ] assert len(set_url_calls) == 1, f"expected one set-url, got {fake.calls}" assert set_url_calls[0][0][4] == "git@github.com:argszero/emrg.git" @@ -1000,7 +1004,7 @@ def test_ensure_origin_reachable_probes_only_once(tmp_path): set_url_calls = [ c for c in fake.calls - if c[0][0] == "git" and c[0][1] == "remote" and c[0][2] == "set-url" + if Path(c[0][0]).name in ("git", "git.exe") and c[0][1] == "remote" and c[0][2] == "set-url" ] assert len(set_url_calls) == 1 @@ -1024,7 +1028,7 @@ def test_ensure_origin_reachable_keeps_https_when_reachable(tmp_path): set_url_calls = [ c for c in fake.calls - if c[0][0] == "git" and c[0][1] == "remote" and c[0][2] == "set-url" + if Path(c[0][0]).name in ("git", "git.exe") and c[0][1] == "remote" and c[0][2] == "set-url" ] assert set_url_calls == [] @@ -1051,7 +1055,7 @@ def test_ensure_origin_reachable_ignores_non_connection_errors(tmp_path): set_url_calls = [ c for c in fake.calls - if c[0][0] == "git" and c[0][1] == "remote" and c[0][2] == "set-url" + if Path(c[0][0]).name in ("git", "git.exe") and c[0][1] == "remote" and c[0][2] == "set-url" ] assert set_url_calls == []