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 <noreply@anthropic.com>
This commit is contained in:
蒋天翔 2026-06-02 23:47:59 +08:00
parent 6e7ee9cd12
commit afed2c9b7e
2 changed files with 180 additions and 0 deletions

View File

@ -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)
},
},
}
}

View File

@ -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)