diff --git a/internal/gitea/files.go b/internal/gitea/files.go index e579dd4..3fcd229 100644 --- a/internal/gitea/files.go +++ b/internal/gitea/files.go @@ -92,13 +92,24 @@ type FileWriteResult struct { } `json:"commit"` } +// UpsertFile creates a file when args.Sha is empty (POST) or updates an existing +// file when args.Sha is set (PUT). Gitea routes both operations by HTTP method on +// the same /contents/{path} URL, and rejects PUT without a sha. func (c *Client) UpsertFile(ctx context.Context, owner, repo, path string, args UpsertFileArgs) (*FileWriteResult, error) { p := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, path) payload, err := json.Marshal(args) if err != nil { return nil, err } - body, status, err := c.PutJSON(ctx, p, payload) + var ( + body []byte + status int + ) + if args.Sha == "" { + body, status, err = c.PostJSON(ctx, p, payload) + } else { + body, status, err = c.PutJSON(ctx, p, payload) + } if err != nil { return nil, err } diff --git a/internal/tools/file_write_branch_test.go b/internal/tools/file_write_branch_test.go index d7f7e12..483a1fd 100644 --- a/internal/tools/file_write_branch_test.go +++ b/internal/tools/file_write_branch_test.go @@ -39,9 +39,9 @@ func TestFileWriteBranchCreatesBranchAndFile(t *testing.T) { _, _ = w.Write([]byte(createBranchResp)) }) - // Upsert file → 201 + // New file (no sha) → POST to /contents/{path} mux.HandleFunc("/api/v1/repos/owner/myrepo/contents/doc.md", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPut, r.Method) + require.Equal(t, http.MethodPost, r.Method) w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(upsertFileResp)) }) @@ -64,6 +64,39 @@ func TestFileWriteBranchCreatesBranchAndFile(t *testing.T) { assert.Equal(t, "cmt1", result["commit_sha"]) } +func TestFileWriteBranchUsesPutWhenShaProvided(t *testing.T) { + mux := http.NewServeMux() + + // Branch exists + mux.HandleFunc("/api/v1/repos/owner/myrepo/branches/feat/existing", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(branchCheckExistsResp)) + }) + + // Existing file (sha provided) → PUT + mux.HandleFunc("/api/v1/repos/owner/myrepo/contents/doc.md", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(upsertFileResp)) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + tool := tools.NewFileWriteBranch(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"owner"})) + out, err := tool.Call(context.Background(), json.RawMessage(`{ + "owner":"owner","name":"myrepo","path":"doc.md", + "content":"hello","branch":"feat/existing", + "sha":"oldsha","message":"update doc.md" + }`)) + require.NoError(t, err) + + var result map[string]any + require.NoError(t, json.Unmarshal(out, &result)) + assert.Equal(t, "feat/existing", result["branch"]) + assert.Equal(t, "cmt1", result["commit_sha"]) +} + func TestFileWriteBranchUsesDefaultBaseWhenBaseEmpty(t *testing.T) { var createBody []byte mux := http.NewServeMux()