From e067aedd0e0978c59f730f40a3bb2ad5feef668a Mon Sep 17 00:00:00 2001 From: tsurutanmen <140517179+tsurutanmen@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:00:27 +0900 Subject: [PATCH 1/2] fix(git): show non-ASCII paths unescaped in git_status and git_diff* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git's default core.quotepath=true makes `git status` and `git diff` print non-ASCII paths as quoted octal escapes, e.g. "\346\227\245\346\234\254\350\252\236.txt" for 日本語.txt. Clients cannot map these back to the real file, so pass `-c core.quotepath=false` for git_status, git_diff_unstaged, git_diff_staged and git_diff. git_show already reports decoded paths through GitPython's Diff objects and is unchanged. Co-Authored-By: Claude Opus 5.5 --- src/git/src/mcp_server_git/server.py | 13 +++++--- src/git/tests/test_server.py | 50 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index b94af84661..76405549ff 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -108,14 +108,19 @@ class GitTools(str, Enum): BRANCH = "git_branch" +def _git_unquoted_paths(repo: git.Repo) -> git.Git: + # git's default core.quotepath=true prints non-ASCII paths as octal escapes + # (e.g. "\346\227\245.txt"), which clients cannot map back to real files. + return repo.git(c="core.quotepath=false") + def git_status(repo: git.Repo) -> str: - return repo.git.status() + return _git_unquoted_paths(repo).status() def git_diff_unstaged(repo: git.Repo, context_lines: int = DEFAULT_CONTEXT_LINES) -> str: - return repo.git.diff(f"--unified={context_lines}") + return _git_unquoted_paths(repo).diff(f"--unified={context_lines}") def git_diff_staged(repo: git.Repo, context_lines: int = DEFAULT_CONTEXT_LINES) -> str: - return repo.git.diff(f"--unified={context_lines}", "--cached") + return _git_unquoted_paths(repo).diff(f"--unified={context_lines}", "--cached") def git_diff(repo: git.Repo, target: str, context_lines: int = DEFAULT_CONTEXT_LINES) -> str: # Defense in depth: reject targets starting with '-' to prevent flag injection, @@ -123,7 +128,7 @@ def git_diff(repo: git.Repo, target: str, context_lines: int = DEFAULT_CONTEXT_L if target.startswith("-"): raise BadName(f"Invalid target: '{target}' - cannot start with '-'") repo.rev_parse(target) # Validates target is a real git ref, throws BadName if not - return repo.git.diff(f"--unified={context_lines}", target) + return _git_unquoted_paths(repo).diff(f"--unified={context_lines}", target) def git_commit(repo: git.Repo, message: str) -> str: commit = repo.index.commit(message) diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 05d5931466..53af472785 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -186,6 +186,56 @@ def test_git_diff(test_repository): assert "test.txt" in result assert "feature changes" in result +NON_ASCII_FILENAME = "日本語.txt" +ESCAPED_NON_ASCII_FILENAME = "\346\227\245\346\234\254\350\252\236.txt" + +@pytest.fixture +def quotepath_repository(test_repository): + # Pin git's default explicitly so the tests don't depend on the user's global config + with test_repository.config_writer() as config: + config.set_value("core", "quotepath", "true") + return test_repository + +def test_git_status_non_ascii_filename(quotepath_repository): + Path(quotepath_repository.working_dir, NON_ASCII_FILENAME).write_text("content", encoding="utf-8") + + result = git_status(quotepath_repository) + + assert NON_ASCII_FILENAME in result + assert ESCAPED_NON_ASCII_FILENAME not in result + +def test_git_diff_unstaged_non_ascii_filename(quotepath_repository): + file_path = Path(quotepath_repository.working_dir, NON_ASCII_FILENAME) + file_path.write_text("content", encoding="utf-8") + quotepath_repository.index.add([NON_ASCII_FILENAME]) + file_path.write_text("modified content", encoding="utf-8") + + result = git_diff_unstaged(quotepath_repository) + + assert f"a/{NON_ASCII_FILENAME}" in result + assert ESCAPED_NON_ASCII_FILENAME not in result + +def test_git_diff_staged_non_ascii_filename(quotepath_repository): + Path(quotepath_repository.working_dir, NON_ASCII_FILENAME).write_text("content", encoding="utf-8") + quotepath_repository.index.add([NON_ASCII_FILENAME]) + + result = git_diff_staged(quotepath_repository) + + assert f"b/{NON_ASCII_FILENAME}" in result + assert ESCAPED_NON_ASCII_FILENAME not in result + +def test_git_diff_non_ascii_filename(quotepath_repository): + default_branch = quotepath_repository.active_branch.name + quotepath_repository.git.checkout("-b", "feature-non-ascii") + Path(quotepath_repository.working_dir, NON_ASCII_FILENAME).write_text("content", encoding="utf-8") + quotepath_repository.index.add([NON_ASCII_FILENAME]) + quotepath_repository.index.commit("add non-ASCII file") + + result = git_diff(quotepath_repository, default_branch) + + assert f"b/{NON_ASCII_FILENAME}" in result + assert ESCAPED_NON_ASCII_FILENAME not in result + def test_git_commit(test_repository): file_path = Path(test_repository.working_dir) / "commit_test.txt" file_path.write_text("content to commit") From 88150d608a16e3ada3ed2d03cd6a8ac053039b80 Mon Sep 17 00:00:00 2001 From: tsurutanmen <140517179+tsurutanmen@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:48:29 +0900 Subject: [PATCH 2/2] test(git): match the escaped name git actually prints The constant used Python octal escapes, so it held 13 characters git never emits and the four not-in assertions passed even before the fix. As a raw string it is the 40-character token git writes. Co-Authored-By: Claude Opus 5.5 --- src/git/tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 53af472785..5969fe1872 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -187,7 +187,7 @@ def test_git_diff(test_repository): assert "feature changes" in result NON_ASCII_FILENAME = "日本語.txt" -ESCAPED_NON_ASCII_FILENAME = "\346\227\245\346\234\254\350\252\236.txt" +ESCAPED_NON_ASCII_FILENAME = r"\346\227\245\346\234\254\350\252\236.txt" @pytest.fixture def quotepath_repository(test_repository):