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")