From 0af0d53decf52f298b3dd21da96304cd58bccbff Mon Sep 17 00:00:00 2001 From: leoca Date: Thu, 17 Sep 2026 07:19:30 +0200 Subject: [PATCH] fix(team): read and write .git/info/exclude as UTF-8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The team worktree manager rewrites the repository's .git/info/exclude with Path.read_text()/write_text() and no encoding, so the locale encoding is used. On Windows (cp1252) a user rule such as "数据/" raises UnicodeDecodeError, which aborts ensure_base() and with it every isolated sub-agent run. Use UTF-8 explicitly, as the hooks config loader already does. --- core/team/worktree.py | 8 ++++---- tests/test_team_worktree.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/team/worktree.py b/core/team/worktree.py index 200be160e..88768af44 100644 --- a/core/team/worktree.py +++ b/core/team/worktree.py @@ -131,12 +131,12 @@ def _install_team_exclude(self) -> None: info = common / "info" info.mkdir(parents=True, exist_ok=True) exclude = info / "exclude" - existing = exclude.read_text() if exclude.exists() else "" + existing = exclude.read_text(encoding="utf-8") if exclude.exists() else "" if _EXCLUDE_BEGIN in existing: return # already installed block = "\n".join((_EXCLUDE_BEGIN, *_TEAM_EXCLUDE, _EXCLUDE_END)) sep = "" if not existing or existing.endswith("\n") else "\n" - exclude.write_text(f"{existing}{sep}{block}\n") + exclude.write_text(f"{existing}{sep}{block}\n", encoding="utf-8") def create(self, worker_id: str) -> str: """Create an isolated worktree on a fresh branch for ``worker_id``.""" @@ -241,10 +241,10 @@ def _remove_team_exclude(self) -> None: exclude = common / "info" / "exclude" if not exclude.exists(): return - text = exclude.read_text() + text = exclude.read_text(encoding="utf-8") if _EXCLUDE_BEGIN not in text: return before, _, rest = text.partition(_EXCLUDE_BEGIN) _, _, after = rest.partition(_EXCLUDE_END) cleaned = (before.rstrip("\n") + "\n" + after.lstrip("\n")).strip("\n") - exclude.write_text(cleaned + "\n" if cleaned else "") + exclude.write_text(cleaned + "\n" if cleaned else "", encoding="utf-8") diff --git a/tests/test_team_worktree.py b/tests/test_team_worktree.py index 343564437..399c51e01 100644 --- a/tests/test_team_worktree.py +++ b/tests/test_team_worktree.py @@ -119,3 +119,20 @@ def test_team_exclude_is_local_idempotent_and_reverted(tmp_path): text = exclude.read_text() assert _EXCLUDE_BEGIN not in text # our block gone assert "user-secret.txt" in text # the user's rule preserved + + +def test_team_exclude_keeps_non_ascii_user_rules(tmp_path): + m = _mgr(tmp_path) + m.ensure_base() + exclude = m.base / ".git" / "info" / "exclude" + m.cleanup_all() + # Users write their own exclude rules as UTF-8. Such a rule must survive + # install/remove whatever the locale encoding is: cp1252, the Windows + # default, cannot even decode these bytes. + original = "数据/\n".encode() + exclude.read_bytes() + exclude.write_bytes(original) + + m.ensure_base() + assert "数据/" in exclude.read_text(encoding="utf-8") + m.cleanup_all() + assert exclude.read_bytes().splitlines() == original.splitlines()