From afed2c9b7e002d4de36ea3a6d0cdbe95a7af5a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E5=A4=A9=E7=BF=94?= Date: Tue, 2 Jun 2026 23:47:59 +0800 Subject: [PATCH] feat: add pr commits/branches/check-merge shortcuts with tests Add three new shortcuts to the pr module: - commits: list commits in a pull request (v1 API) - branches: list branches for PR creation - check-merge: check if two branches can be merged Each shortcut includes unit tests covering both success and HTTP error paths. Co-Authored-By: Claude Opus 4.7 --- shortcuts/pr/pr.go | 66 +++++++++++++++++++++++ shortcuts/pr/pr_test.go | 114 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 5fd983b..0837ba6 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -364,6 +364,72 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "commits", + Description: "List commits in a pull request", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "PR number", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", prV1Path(ctx, id)+"/commits", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "branches", + Description: "List branches for pull request creation", + Flags: []common.Flag{}, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/pulls/get_branches", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "check-merge", + Description: "Check if two branches can be merged", + Flags: []common.Flag{ + {Name: "head", Usage: "Source branch", Required: true}, + {Name: "base", Usage: "Target branch", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + head, err := ctx.RequireArg("head") + if err != nil { + return err + } + base, err := ctx.RequireArg("base") + if err != nil { + return err + } + payload := map[string]interface{}{ + "head": head, + "base": base, + } + env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls/check_can_merge", payload) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index 6a86afe..3df6671 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -426,6 +426,120 @@ func TestPRDiffHTTPError(t *testing.T) { } } +// --- commits --- + +func TestPRCommits(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/v1/owner/repo/pulls/42/commits.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, []interface{}{ + map[string]interface{}{"sha": "abc1234", "message": "fix: bug"}, + }) + })) + defer server.Close() + + err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"}) + if err != nil { + t.Fatalf("commits failed: %v", err) + } +} + +func TestPRCommitsHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- branches --- + +func TestPRBranches(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/pulls/get_branches.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, []interface{}{ + map[string]interface{}{"name": "master"}, + map[string]interface{}{"name": "develop"}, + }) + })) + defer server.Close() + + err := runPRShortcut(t, server, "branches", map[string]string{}) + if err != nil { + t.Fatalf("branches failed: %v", err) + } +} + +func TestPRBranchesHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runPRShortcut(t, server, "branches", map[string]string{}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- check-merge --- + +func TestPRCheckMerge(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/owner/repo/pulls/check_can_merge.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"can_merge": true}) + })) + defer server.Close() + + err := runPRShortcut(t, server, "check-merge", map[string]string{ + "head": "feature/x", + "base": "master", + }) + if err != nil { + t.Fatalf("check-merge failed: %v", err) + } + assertEqual(t, payload["head"], "feature/x") + assertEqual(t, payload["base"], "master") +} + +func TestPRCheckMergeHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runPRShortcut(t, server, "check-merge", map[string]string{ + "head": "feature/x", + "base": "master", + }) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + func runPRShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { t.Helper() shortcut := findPRShortcut(t, name)