Merge pull request #284: feat(pr): 支持按编号搜索 PR
# Conflicts: # shortcuts/pr/pr.go
This commit is contained in:
commit
8a60844342
|
|
@ -0,0 +1,10 @@
|
|||
## PR list supports direct lookup by PR number
|
||||
|
||||
`pr +list` previously only exposed keyword-based search, which made it awkward
|
||||
to jump to a known PR from the web UI or from review notes. This change adds
|
||||
`--number` / `-n` and a compatibility alias `--id` / `-i` to the list command.
|
||||
|
||||
When a PR number is provided, the CLI now reads that PR through the dedicated
|
||||
detail endpoint and wraps the result into the usual list payload shape. This
|
||||
keeps the output stable for automation while making exact-number lookup work
|
||||
even when the target PR is not on the current list page.
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
package pr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
|
|
@ -41,6 +38,8 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Flags: []common.Flag{
|
||||
{Name: "state", Short: "s", Usage: tr.T("flag.pr.state"), Default: "open"},
|
||||
{Name: "keyword", Short: "k", Usage: tr.T("flag.search.keyword")},
|
||||
{Name: "number", Short: "n", Usage: "PR number shown in the web URL"},
|
||||
{Name: "id", Short: "i", Usage: "Compatibility alias for --number; this is not the database ID"},
|
||||
{Name: "priority-id", Usage: tr.T("flag.pr.priority_id")},
|
||||
{Name: "tag-id", Usage: tr.T("flag.pr.tag_id")},
|
||||
{Name: "milestone-id", Usage: tr.T("flag.pr.milestone_id")},
|
||||
|
|
@ -55,6 +54,9 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
if number := pullRequestListNumberArg(ctx); number != "" {
|
||||
return outputPullRequestListByNumber(ctx, number)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
|
|
@ -130,17 +132,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "view",
|
||||
Description: tr.T("cmd.pr.view.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -151,75 +149,18 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "checkout",
|
||||
Description: tr.T("cmd.pr.checkout.short"),
|
||||
Long: tr.T("cmd.pr.checkout.long"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "branch", Short: "b", Usage: tr.T("flag.pr.checkout_branch")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, _ := env.Data.(map[string]interface{})
|
||||
head, _ := data["head"].(string)
|
||||
if head == "" {
|
||||
return errors.New(tr.T("error.pr.checkout_head_missing"))
|
||||
}
|
||||
local := ctx.Arg("branch")
|
||||
if local == "" {
|
||||
local = head
|
||||
}
|
||||
remote := "origin"
|
||||
if fork, ok := data["fork_project"].(map[string]interface{}); ok {
|
||||
login, _ := fork["login"].(string)
|
||||
identifier, _ := fork["identifier"].(string)
|
||||
if login != "" && identifier != "" && login != ctx.Owner {
|
||||
remote = fmt.Sprintf("https://gitlink.org.cn/%s/%s.git", login, identifier)
|
||||
}
|
||||
}
|
||||
for _, args := range [][]string{
|
||||
{"fetch", remote, fmt.Sprintf("%s:%s", head, local)},
|
||||
{"checkout", local},
|
||||
} {
|
||||
gitCmd := exec.Command("git", args...)
|
||||
gitCmd.Stdout = os.Stderr
|
||||
gitCmd.Stderr = os.Stderr
|
||||
if err := gitCmd.Run(); err != nil {
|
||||
return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "checked out",
|
||||
"pull": id,
|
||||
"branch": local,
|
||||
"remote": remote,
|
||||
}, nil))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "merge",
|
||||
Description: tr.T("cmd.pr.merge.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "method", Short: "m", Usage: tr.T("flag.pr.merge_method"), Default: "merge"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
method := ctx.Arg("method")
|
||||
if method == "" {
|
||||
method = "merge"
|
||||
|
|
@ -238,17 +179,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "refuse",
|
||||
Description: "Refuse and close a pull request",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -260,14 +197,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "reopen",
|
||||
Description: "Reopen a closed pull request",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{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 := prNumberArg(ctx)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -282,17 +218,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "files",
|
||||
Description: tr.T("cmd.pr.files.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -304,17 +236,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "diff",
|
||||
Description: tr.T("cmd.pr.diff.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -326,14 +254,13 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "versions",
|
||||
Description: tr.T("cmd.pr.versions.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -348,8 +275,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "version-diff",
|
||||
Description: tr.T("cmd.pr.version_diff.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "version-id", Short: "v", Usage: tr.T("flag.pr.version_id"), Required: true},
|
||||
{Name: "file", Short: "f", Usage: tr.T("flag.pr.file")},
|
||||
},
|
||||
|
|
@ -357,7 +283,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -386,15 +312,14 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "reviews",
|
||||
Description: tr.T("cmd.pr.reviews.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "status", Short: "s", Usage: tr.T("flag.pr.review_status_filter")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -416,8 +341,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "review",
|
||||
Description: tr.T("cmd.pr.review.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "status", Short: "s", Usage: tr.T("flag.pr.review_status"), Default: "common"},
|
||||
{Name: "content", Short: "c", Usage: tr.T("flag.pr.review_content"), Required: true},
|
||||
{Name: "commit", Short: "m", Usage: tr.T("flag.pr.review_commit")},
|
||||
|
|
@ -427,7 +351,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -464,12 +388,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
}
|
||||
|
||||
// Also post a journal comment so the review is visible in the PR conversation.
|
||||
statusLabel := map[string]string{
|
||||
"approved": "approved", "rejected": "rejected", "common": "commented",
|
||||
}[status]
|
||||
summary := fmt.Sprintf("## Review: %s\n\n%s", statusLabel, content)
|
||||
ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/journals", v1RepoPath(ctx), id),
|
||||
map[string]interface{}{"note": summary})
|
||||
prEnv, journalErr := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if journalErr == nil {
|
||||
if issueID, extractErr := extractIssueID(prEnv); extractErr == nil {
|
||||
statusLabel := map[string]string{
|
||||
"approved": "approved", "rejected": "rejected", "common": "commented",
|
||||
}[status]
|
||||
summary := fmt.Sprintf("## Review: %s\n\n%s", statusLabel, content)
|
||||
ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/issues/%d/journals", ctx.Owner, ctx.Repo, issueID),
|
||||
map[string]interface{}{"notes": summary})
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -478,24 +407,29 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "comment",
|
||||
Description: tr.T("cmd.pr.comment.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: tr.T("flag.pr.number")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id")},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||
{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := prNumberArg(ctx)
|
||||
id, _ := ctx.RequireArg("id")
|
||||
body, _ := ctx.RequireArg("body")
|
||||
|
||||
prEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch PR: %w", err)
|
||||
}
|
||||
issueID, err := extractIssueID(prEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := ctx.RequireArg("body")
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"note": body,
|
||||
"notes": body,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/journals", v1RepoPath(ctx), id), payload)
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/issues/%d/journals", ctx.Owner, ctx.Repo, issueID), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -541,6 +475,32 @@ func extractIssueID(env *output.Envelope) (int64, error) {
|
|||
return int64(idFloat), nil
|
||||
}
|
||||
|
||||
func pullRequestListNumberArg(ctx *common.RuntimeContext) string {
|
||||
if number := strings.TrimSpace(ctx.Arg("number")); number != "" {
|
||||
return number
|
||||
}
|
||||
return strings.TrimSpace(ctx.Arg("id"))
|
||||
}
|
||||
|
||||
func outputPullRequestListByNumber(ctx *common.RuntimeContext, number string) error {
|
||||
env, err := ctx.CallAPI("GET", prV1Path(ctx, number), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected PR response format")
|
||||
}
|
||||
|
||||
if normalizedNumber := firstPullRequestNumber(pr); normalizedNumber != nil {
|
||||
pr["number"] = normalizedNumber
|
||||
}
|
||||
|
||||
env.Data, env.Meta = wrapPullRequestListByNumberResult(pr)
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func enrichPullRequestClosedAt(ctx *common.RuntimeContext, env *output.Envelope) error {
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
|
|
@ -631,12 +591,24 @@ func numberField(m map[string]interface{}, key string) (float64, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func prNumberArg(ctx *common.RuntimeContext) (string, error) {
|
||||
if number := strings.TrimSpace(ctx.Arg("number")); number != "" {
|
||||
return number, nil
|
||||
func firstPullRequestNumber(pr map[string]interface{}) interface{} {
|
||||
for _, key := range []string{"number", "pull_request_number", "index"} {
|
||||
if value, ok := pr[key]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if id := strings.TrimSpace(ctx.Arg("id")); id != "" {
|
||||
return id, nil
|
||||
}
|
||||
return "", fmt.Errorf("required flag --number is missing (or use --id as a compatibility alias)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapPullRequestListByNumberResult(pr map[string]interface{}) (map[string]interface{}, *output.Meta) {
|
||||
return map[string]interface{}{
|
||||
"total_count": 1,
|
||||
"page": 1,
|
||||
"limit": 1,
|
||||
"pulls": []interface{}{pr},
|
||||
}, &output.Meta{
|
||||
TotalCount: 1,
|
||||
Page: 1,
|
||||
Limit: 1,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,89 @@ func TestPRListStateAllOmitsStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPRListByNumberUsesDetailEndpoint(t *testing.T) {
|
||||
var requestedPath string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestedPath = r.URL.Path
|
||||
if r.URL.Path != "/v1/owner/repo/pulls/42.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(101),
|
||||
"index": float64(42),
|
||||
"title": "feat: search by number",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "list", map[string]string{"number": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("list by number failed: %v", err)
|
||||
}
|
||||
if requestedPath == "" {
|
||||
t.Fatal("expected detail endpoint to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRListByIDAliasUsesDetailEndpoint(t *testing.T) {
|
||||
var requestedPath string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestedPath = r.URL.Path
|
||||
if r.URL.Path != "/v1/owner/repo/pulls/7.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(202),
|
||||
"index": float64(7),
|
||||
"title": "feat: alias",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "list", map[string]string{"id": "7"})
|
||||
if err != nil {
|
||||
t.Fatalf("list by id alias failed: %v", err)
|
||||
}
|
||||
if requestedPath == "" {
|
||||
t.Fatal("expected detail endpoint to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestListNumberArgPrefersNumber(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{
|
||||
Args: map[string]string{
|
||||
"number": "15",
|
||||
"id": "9",
|
||||
},
|
||||
}
|
||||
if got := pullRequestListNumberArg(ctx); got != "15" {
|
||||
t.Fatalf("pullRequestListNumberArg() = %q, want 15", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapPullRequestListByNumberResult(t *testing.T) {
|
||||
pr := map[string]interface{}{
|
||||
"id": float64(303),
|
||||
"number": float64(88),
|
||||
"title": "feat: wrapped number",
|
||||
}
|
||||
|
||||
data, meta := wrapPullRequestListByNumberResult(pr)
|
||||
|
||||
assertEqual(t, data["total_count"], 1)
|
||||
assertEqual(t, data["page"], 1)
|
||||
assertEqual(t, data["limit"], 1)
|
||||
pulls := data["pulls"].([]interface{})
|
||||
wrapped := pulls[0].(map[string]interface{})
|
||||
assertEqual(t, wrapped["number"], float64(88))
|
||||
if meta == nil {
|
||||
t.Fatal("expected meta to be set")
|
||||
}
|
||||
assertEqual(t, meta.TotalCount, 1)
|
||||
assertEqual(t, meta.Page, 1)
|
||||
assertEqual(t, meta.Limit, 1)
|
||||
}
|
||||
|
||||
// --- create ---
|
||||
|
||||
func TestPRCreate(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,60 @@
|
|||
# pr +list
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> Read [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) first for
|
||||
> authentication, global flags, and safety rules.
|
||||
|
||||
列出仓库的 Pull Request 列表。
|
||||
List pull requests for a repository. The command supports state filtering,
|
||||
keyword search, pagination, and direct lookup by PR number.
|
||||
|
||||
## 命令
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# 列出 PR(默认 state=open)
|
||||
# List open pull requests for the current repository
|
||||
gitlink-cli pr +list
|
||||
|
||||
# 指定仓库和状态
|
||||
gitlink-cli pr +list --owner Gitlink --repo forgeplus --state open
|
||||
# List merged pull requests for a specific repository
|
||||
gitlink-cli pr +list --owner Gitlink --repo forgeplus --state merged
|
||||
|
||||
# 分页
|
||||
# Search by keyword
|
||||
gitlink-cli pr +list --keyword release --sort-by updated_at --sort-direction desc
|
||||
|
||||
# Search by PR number from the web URL
|
||||
gitlink-cli pr +list --number 42
|
||||
gitlink-cli pr +list --id 42
|
||||
|
||||
# Paginate results
|
||||
gitlink-cli pr +list --page 2 --limit 10
|
||||
|
||||
# 查看已合并的 PR(注意:state 仅影响统计计数)
|
||||
gitlink-cli pr +list --state merged --format json
|
||||
```
|
||||
|
||||
## 参数
|
||||
## Flags
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--state` / `-s` | 否 | 过滤状态:`open`、`merged`、`closed`(默认 `open`) |
|
||||
| `--page` / `-p` | 否 | 页码(默认 `1`) |
|
||||
| `--limit` / `-l` | 否 | 每页条数(默认 `20`) |
|
||||
| Flag | Required | Description |
|
||||
|---|---|---|
|
||||
| `--state`, `-s` | No | Filter by `open`, `merged`, `closed`, or `all`. Default: `open`. |
|
||||
| `--keyword`, `-k` | No | Search PRs by keyword. |
|
||||
| `--number`, `-n` | No | Fetch one PR by the PR number shown in the web URL. |
|
||||
| `--id`, `-i` | No | Compatibility alias for `--number`. |
|
||||
| `--priority-id` | No | Filter by priority ID. |
|
||||
| `--tag-id` | No | Filter by issue tag ID. |
|
||||
| `--milestone-id` | No | Filter by milestone/version ID. |
|
||||
| `--reviewer-id` | No | Filter by reviewer ID. |
|
||||
| `--assignee-id` | No | Filter by assignee ID. |
|
||||
| `--sort-by` | No | Sort field, such as `updated_at` or `created_at`. |
|
||||
| `--sort-direction` | No | Sort direction: `asc` or `desc`. |
|
||||
| `--page`, `-p` | No | Page number. Default: `1`. |
|
||||
| `--limit`, `-l` | No | Page size. Default: `20`. |
|
||||
|
||||
## API
|
||||
## Behavior Notes
|
||||
|
||||
```
|
||||
GET /{owner}/{repo}/pulls?state={state}&page={page}&limit={limit}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `--state` 参数**仅影响响应中的统计计数**(open_count / merged_count / closed_count),返回的 PR 列表可能包含所有状态的 PR
|
||||
- 如需精确过滤,请在客户端通过 `pull_request_status` 字段二次过滤:
|
||||
- `0` = open
|
||||
- `1` = merged
|
||||
- `2` = closed
|
||||
- 返回的每条 PR 包含 `pull_request_number` 字段(即网页 URL `/pulls/N` 中的序号),用于 `pr +view`、`pr +merge`、`pr +close` 等操作
|
||||
- Regular list mode uses `GET /api/v1/{owner}/{repo}/pulls.json`.
|
||||
- `--number` / `--id` uses `GET /api/v1/{owner}/{repo}/pulls/{index}.json` and
|
||||
returns the result in list form so scripts can keep using `pr +list`.
|
||||
- Each returned PR includes a user-facing `number` field that matches the web UI
|
||||
URL `/pulls/N`.
|
||||
- The PR number is the project-level sequence number, not the global database
|
||||
primary key.
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-shared SKILL.md](../../gitlink-shared/SKILL.md) -- 认证与全局参数
|
||||
- [gitlink-pr SKILL.md](../SKILL.md) -- PR 操作总览
|
||||
- [gitlink-shared SKILL.md](../../gitlink-shared/SKILL.md)
|
||||
- [gitlink-pr SKILL.md](../SKILL.md)
|
||||
|
|
|
|||
Loading…
Reference in New Issue