diff --git a/internal/gitea/client.go b/internal/gitea/client.go index 908e5a1..415e8e9 100644 --- a/internal/gitea/client.go +++ b/internal/gitea/client.go @@ -972,6 +972,40 @@ func (c *Client) SetIssueState(ctx context.Context, token, owner, repo string, i return nil } +// MergePull merges a pull request using the specified merge style. +// Valid styles: "merge", "rebase", "rebase-merge", "squash". +// If style is empty, defaults to "merge". +func (c *Client) MergePull(ctx context.Context, token, owner, repo string, index int64, style, title, message string) error { + if style == "" { + style = "merge" + } + + payload := map[string]string{ + "Do": style, + } + if title != "" { + payload["merge_message_field"] = title + } + if message != "" { + payload["merge_message_field"] = message + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshaling merge request: %w", err) + } + + path := fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", owner, repo, index) + resp, err := c.doRequest(ctx, token, http.MethodPost, path, strings.NewReader(string(jsonData))) + if err != nil { + return fmt.Errorf("merging pull request: %w", err) + } + resp.Body.Close() + + c.InvalidateAll() + return nil +} + // AddComment creates a comment on an issue and returns the created Comment. func (c *Client) AddComment(ctx context.Context, token, owner, repo string, index int64, body string) (*Comment, error) { return c.PostComment(ctx, token, owner, repo, index, body) diff --git a/internal/gitea/client_test.go b/internal/gitea/client_test.go index 87b1196..04dec22 100644 --- a/internal/gitea/client_test.go +++ b/internal/gitea/client_test.go @@ -1516,3 +1516,81 @@ func TestGetChangedFiles_Error(t *testing.T) { t.Errorf("error should contain status code 404, got: %v", err) } } + +func TestMergePull(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/owner1/repo1/pulls/5/merge" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.Header.Get("Authorization") != "token test-token" { + t.Error("missing or wrong Authorization header") + } + + var body map[string]string + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode body: %v", err) + } + if body["Do"] != "squash" { + t.Errorf("expected Do=squash, got %q", body["Do"]) + } + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + c := NewClient(server.URL) + c.setCache("pulls-org1", "should-be-invalidated") + + err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "squash", "", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify cache was invalidated. + _, ok := c.getFromCache("pulls-org1") + if ok { + t.Error("expected cache to be invalidated after MergePull") + } +} + +func TestMergePull_DefaultStyle(t *testing.T) { + var receivedStyle string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body map[string]string + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode body: %v", err) + } + receivedStyle = body["Do"] + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + c := NewClient(server.URL) + err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "", "", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if receivedStyle != "merge" { + t.Errorf("expected default style 'merge', got %q", receivedStyle) + } +} + +func TestMergePull_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprintln(w, `{"message":"not mergeable"}`) + })) + defer server.Close() + + c := NewClient(server.URL) + err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "merge", "", "") + if err == nil { + t.Fatal("expected error for 405 response, got nil") + } + if !strings.Contains(err.Error(), "405") { + t.Errorf("error should contain status code 405, got: %v", err) + } +}