feat(pr): 支持按编号搜索 PR
This commit is contained in:
parent
71ca2bb683
commit
ffb35a4f02
|
|
@ -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.
|
||||
|
|
@ -38,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")},
|
||||
|
|
@ -52,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"))
|
||||
|
|
@ -470,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 {
|
||||
|
|
@ -559,3 +590,25 @@ func numberField(m map[string]interface{}, key string) (float64, bool) {
|
|||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func firstPullRequestNumber(pr map[string]interface{}) interface{} {
|
||||
for _, key := range []string{"number", "pull_request_number", "index"} {
|
||||
if value, ok := pr[key]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,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