From 7d22bc125b6822b9b8233f078567163bc561b6e6 Mon Sep 17 00:00:00 2001 From: silverwind <2021+silverwind@noreply.gitea.com> Date: Sun, 9 Aug 2026 10:10:58 +0000 Subject: [PATCH] fix: honor `new_branch_name` on the file update path (#228) The update path of `create_or_update_file` built `UpdateFileOptions` without `NewBranchName`, so a call asking for a new branch committed straight to the base branch and still reported success. The success text now names the branch that got the commit. Fixes: https://gitea.com/gitea/gitea-mcp/issues/217 Co-authored-by: bircni Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/228 Reviewed-by: bircni Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com> --- operation/repo/file.go | 34 ++++++++-------- operation/repo/file_test.go | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 operation/repo/file_test.go diff --git a/operation/repo/file.go b/operation/repo/file.go index 9a9d7a2..af10c07 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -3,6 +3,7 @@ package repo import ( "bufio" "bytes" + "cmp" "context" "encoding/base64" "encoding/json" @@ -61,7 +62,7 @@ var ( tool.String("message", tool.Required(), tool.Description("commit message")), tool.String("branch_name", tool.Required()), tool.String("sha", tool.Description("existing file SHA (omit to create)")), - tool.String("new_branch_name", tool.Description("new branch (create only)")), + tool.String("new_branch_name", tool.Description("branch to create from branch_name and commit to")), ) DeleteFileTool = tool.NewDefinition( @@ -204,6 +205,7 @@ func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallTo content, _ := args["content"].(string) message, _ := args["message"].(string) branchName, _ := args["branch_name"].(string) + newBranchName, _ := args["new_branch_name"].(string) sha, _ := args["sha"].(string) client, err := gitea.ClientFromContext(ctx) @@ -211,39 +213,37 @@ func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallTo return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } + fileOpt := gitea_sdk.FileOptions{ + Message: message, + BranchName: branchName, + NewBranchName: newBranchName, + } + targetBranch := cmp.Or(newBranchName, branchName) + if sha != "" { // Update existing file opt := gitea_sdk.UpdateFileOptions{ - SHA: sha, - Content: base64.StdEncoding.EncodeToString([]byte(content)), - FileOptions: gitea_sdk.FileOptions{ - Message: message, - BranchName: branchName, - }, + SHA: sha, + Content: base64.StdEncoding.EncodeToString([]byte(content)), + FileOptions: fileOpt, } _, _, err = client.Repositories.UpdateFile(ctx, owner, repo, filePath, opt) if err != nil { return to.ErrorResult(fmt.Errorf("update file err: %v", err)) } - return to.TextResult("Update file success") + return to.TextResult("Update file success on branch " + targetBranch) } // Create new file opt := gitea_sdk.CreateFileOptions{ - Content: base64.StdEncoding.EncodeToString([]byte(content)), - FileOptions: gitea_sdk.FileOptions{ - Message: message, - BranchName: branchName, - }, - } - if newBranch, ok := args["new_branch_name"].(string); ok && newBranch != "" { - opt.NewBranchName = newBranch + Content: base64.StdEncoding.EncodeToString([]byte(content)), + FileOptions: fileOpt, } _, _, err = client.Repositories.CreateFile(ctx, owner, repo, filePath, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create file err: %v", err)) } - return to.TextResult("Create file success") + return to.TextResult("Create file success on branch " + targetBranch) } func DeleteFileFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { diff --git a/operation/repo/file_test.go b/operation/repo/file_test.go new file mode 100644 index 0000000..4003d67 --- /dev/null +++ b/operation/repo/file_test.go @@ -0,0 +1,81 @@ +package repo + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "gitea.com/gitea/gitea-mcp/pkg/flag" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestCreateOrUpdateFileFnNewBranch(t *testing.T) { + const ( + owner = "octo" + repo = "demo" + filePath = "README.md" + baseBranch = "main" + newBranch = "feature-x" + ) + + var ( + mu sync.Mutex + gotBody map[string]any + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + mu.Lock() + gotBody = body + mu.Unlock() + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + origHost, origToken := flag.Host, flag.Token + flag.Host, flag.Token = server.URL, "" + defer func() { flag.Host, flag.Token = origHost, origToken }() + + for _, tc := range []struct { + name string + sha string + wantResult string + }{ + {"create", "", "Create file success on branch " + newBranch}, + {"update", "blobsha", "Update file success on branch " + newBranch}, + } { + t.Run(tc.name, func(t *testing.T) { + result, err := CreateOrUpdateFileFn(context.Background(), map[string]any{ + "owner": owner, + "repo": repo, + "path": filePath, + "content": "hello", + "message": "update readme", + "branch_name": baseBranch, + "new_branch_name": newBranch, + "sha": tc.sha, + }) + if err != nil { + t.Fatalf("CreateOrUpdateFileFn() error = %v", err) + } + + mu.Lock() + defer mu.Unlock() + if gotBody["new_branch"] != newBranch { + t.Fatalf("new_branch = %v, want %s", gotBody["new_branch"], newBranch) + } + if gotBody["branch"] != baseBranch { + t.Fatalf("branch = %v, want %s", gotBody["branch"], baseBranch) + } + if text := result.Content[0].(*mcp.TextContent).Text; !strings.Contains(text, tc.wantResult) { + t.Fatalf("result = %s, want it to contain %q", text, tc.wantResult) + } + }) + } +}