Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/git/src/mcp_server_git/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,27 @@ 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,
# even if a malicious ref with that name exists (e.g. via filesystem manipulation)
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)
Expand Down
50 changes: 50 additions & 0 deletions src/git/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading