From 87ead3afd87b8ae2f0af20cf32699a0499b475b1 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:50:14 +0530 Subject: [PATCH] fix(repos): escape the path in create_or_update_file A path containing # or ? was sent unescaped, so the file was written to the part of the path before that character (docs/C#/intro.md -> docs/C). --- pkg/github/repositories.go | 3 ++- pkg/github/repositories_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 4b7e84cb9a..52298f127c 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -605,7 +605,8 @@ SHA MUST be provided for existing file updates. // If file not found, no previous SHA needed (new file creation) } - fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) + // CreateFile puts path into the URL as is, unlike GetContents. + fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, escapeGitTreeish(path), opts) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to create/update file", diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 0c930ae8c0..1f0ad310f0 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -6823,3 +6823,36 @@ func Test_ListRepositoryCollaborators(t *testing.T) { }) } } + +func Test_CreateOrUpdateFile_EscapesPath(t *testing.T) { + serverTool := CreateOrUpdateFile(translations.NullTranslationHelper) + + var putPath string + mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + "": func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + putPath = r.URL.EscapedPath() + mockResponse(t, http.StatusCreated, &github.RepositoryContentResponse{ + Content: &github.RepositoryContent{Path: new("docs/C#/intro?.md")}, + })(w, r) + return + } + mockResponse(t, http.StatusNotFound, map[string]string{"message": "Not Found"})(w, r) + }, + }) + deps := BaseDeps{Client: mustNewGHClient(t, mockedClient)} + handler := serverTool.Handler(deps) + + request := createMCPRequest(map[string]any{ + "owner": "owner", + "repo": "repo", + "path": "docs/C#/intro?.md", + "content": "# Intro", + "message": "Add intro", + "branch": "main", + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError, getTextResult(t, result).Text) + assert.Equal(t, "/repos/owner/repo/contents/docs/C%23/intro%3F.md", putPath) +}