From fbea42ecceded99744c90b8cf9a458c8746fa6c4 Mon Sep 17 00:00:00 2001 From: wangyue789 Date: Thu, 21 May 2026 17:20:15 +0800 Subject: [PATCH] feat(pr): add review shortcuts --- README.md | 7 ++ README.zh-CN.md | 7 ++ shortcuts/pr/pr.go | 94 ++++++++++++++++++ shortcuts/pr/pr_test.go | 99 +++++++++++++++++++ skills/README.md | 2 +- skills/gitlink-pr/SKILL.md | 22 ++++- .../references/gitlink-pr-review.md | 73 ++++++++++++++ .../references/gitlink-pr-reviews.md | 61 ++++++++++++ 8 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 skills/gitlink-pr/references/gitlink-pr-review.md create mode 100644 skills/gitlink-pr/references/gitlink-pr-reviews.md diff --git a/README.md b/README.md index cb2663d..56b6744 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,13 @@ gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42 # View changed files gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 + +# List PR reviews +gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42 + +# Approve a PR after previewing the request +gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run +gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" ``` ### Branch Management diff --git a/README.zh-CN.md b/README.zh-CN.md index 4531992..d78e88d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -200,6 +200,13 @@ gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42 # 查看 PR 变更文件 gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 + +# 查看 PR 审查记录 +gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42 + +# 预览并通过 PR +gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run +gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" ``` ### 发布管理 diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 5f4a713..08300b2 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -167,6 +167,87 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "reviews", + Description: "List pull request reviews", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "PR number", Required: true}, + {Name: "status", Short: "s", Usage: "Filter review status: common, approved, rejected"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + q := url.Values{} + if status := ctx.Arg("status"); status != "" { + if err := validatePRReviewStatus(status); err != nil { + return err + } + q.Set("status", status) + } + env, err := ctx.CallAPIWithQuery("GET", prReviewsPath(ctx, id), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "review", + Description: "Create a pull request review", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "PR number", Required: true}, + {Name: "status", Short: "s", Usage: "Review status: common, approved, rejected", Default: "common"}, + {Name: "content", Short: "c", Usage: "Review content", Required: true}, + {Name: "commit", Short: "m", Usage: "Commit SHA to attach the review to"}, + {Name: "dry-run", Usage: "Preview the review request without creating it", Bool: true, Default: "false"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + content, err := ctx.RequireArg("content") + if err != nil { + return err + } + status := ctx.Arg("status") + if status == "" { + status = "common" + } + if err := validatePRReviewStatus(status); err != nil { + return err + } + payload := map[string]interface{}{ + "content": content, + "status": status, + } + if commit := ctx.Arg("commit"); commit != "" { + payload["commit_id"] = commit + } + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), + "pull_request": id, + "dry_run": true, + "action": "create_review", + "payload": payload, + }) + } + env, err := ctx.CallAPI("POST", prReviewsPath(ctx, id), payload) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "comment", Description: "Add a comment to a pull request", @@ -203,6 +284,19 @@ func Shortcuts() []*common.Shortcut { } } +func prReviewsPath(ctx *common.RuntimeContext, id string) string { + return fmt.Sprintf("/v1/%s/%s/pulls/%s/reviews", ctx.Owner, ctx.Repo, id) +} + +func validatePRReviewStatus(status string) error { + switch status { + case "common", "approved", "rejected": + return nil + default: + return fmt.Errorf("invalid --status value %q: use common, approved, or rejected", status) + } +} + func extractIssueID(env *output.Envelope) (int64, error) { data, ok := env.Data.(map[string]interface{}) if !ok { diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index b80ab60..24d34c5 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -91,6 +91,105 @@ func TestPRCommentFailsWhenIssueFieldMissing(t *testing.T) { } } +func TestPRReviewsUsesV1EndpointWithStatusFilter(t *testing.T) { + var calledPath string + var status string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/pulls/13/reviews.json" { + t.Fatalf("unexpected request: %s %s?%s", r.Method, r.URL.Path, r.URL.RawQuery) + } + calledPath = r.URL.Path + status = r.URL.Query().Get("status") + writeJSON(t, w, map[string]interface{}{ + "total_count": float64(1), + "reviews": []map[string]interface{}{ + { + "id": float64(100), + "content": "LGTM", + "status": "approved", + }, + }, + }) + })) + defer server.Close() + + err := runPRShortcut(t, server, "reviews", map[string]string{ + "id": "13", + "status": "approved", + }) + if err != nil { + t.Fatalf("reviews shortcut failed: %v", err) + } + assertEqual(t, calledPath, "/v1/owner/repo/pulls/13/reviews.json") + assertEqual(t, status, "approved") +} + +func TestPRReviewPostsReviewPayload(t *testing.T) { + var reviewPayload map[string]interface{} + var reviewPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/pulls/13/reviews.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + reviewPath = r.URL.Path + reviewPayload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{ + "id": float64(101), + "content": "Looks good", + "status": "approved", + "commit_id": "abc123", + }) + })) + defer server.Close() + + err := runPRShortcut(t, server, "review", map[string]string{ + "id": "13", + "status": "approved", + "content": "Looks good", + "commit": "abc123", + }) + if err != nil { + t.Fatalf("review shortcut failed: %v", err) + } + assertEqual(t, reviewPath, "/v1/owner/repo/pulls/13/reviews.json") + assertEqual(t, reviewPayload["content"], "Looks good") + assertEqual(t, reviewPayload["status"], "approved") + assertEqual(t, reviewPayload["commit_id"], "abc123") +} + +func TestPRReviewDryRunDoesNotCallAPI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("server should not be called during dry-run: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runPRShortcut(t, server, "review", map[string]string{ + "id": "13", + "status": "rejected", + "content": "Please fix the failing tests", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("review dry-run failed: %v", err) + } +} + +func TestPRReviewRejectsInvalidStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("server should not be called for invalid status: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runPRShortcut(t, server, "review", map[string]string{ + "id": "13", + "status": "approve", + "content": "LGTM", + }) + if err == nil { + t.Fatal("expected error for invalid review status, got nil") + } +} + func runPRShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { t.Helper() shortcut := findPRShortcut(t, name) diff --git a/skills/README.md b/skills/README.md index 2786e1d..7430680 100644 --- a/skills/README.md +++ b/skills/README.md @@ -123,7 +123,7 @@ skills/ | **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` | | **gitlink-repo** | 仓库管理 | `repo +list`, `repo +create`, `repo +info`, `repo +fork` | | **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` | -| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +review` | +| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +reviews`, `pr +review` | | **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` | | **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` | diff --git a/skills/gitlink-pr/SKILL.md b/skills/gitlink-pr/SKILL.md index 3462ca2..565d223 100644 --- a/skills/gitlink-pr/SKILL.md +++ b/skills/gitlink-pr/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-pr version: 1.0.0 -description: "Pull Request 管理:创建、查看、合并、关闭 PR,查看变更文件和 Diff。当用户需要操作 GitLink PR 时触发。" +description: "Pull Request 管理:创建、查看、合并、关闭 PR,查看变更文件、Diff 和审查记录。当用户需要操作 GitLink PR 时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -25,7 +25,9 @@ metadata: | `pr +merge` | 合并 PR | 是 | | `pr +close` | 关闭 PR | 是 | | `pr +files` | 变更文件列表 | 否 | -| `pr +diff` | 查看提交列表 | 否 | +| `pr +diff` | 查看变更文件和 diff 内容 | 否 | +| `pr +reviews` | 查看 PR 审查记录 | 否(公开项目) | +| `pr +review` | 创建 PR 审查(comment/approve/reject) | 是 | | `pr +comment` | 给 PR 添加评论 | 是 | ## 使用示例 @@ -50,6 +52,15 @@ gitlink-cli pr +close --id 3 # 查看变更文件(含 diff 内容) gitlink-cli pr +files --id 3 +# 查看 PR 审查记录 +gitlink-cli pr +reviews --id 3 +gitlink-cli pr +reviews --id 3 --status approved + +# 创建 PR 审查(先 dry-run,再真实提交) +gitlink-cli pr +review --id 3 --status approved --content "LGTM" --dry-run +gitlink-cli pr +review --id 3 --status approved --content "LGTM" +gitlink-cli pr +review --id 3 --status rejected --content "测试未通过,请修复后再合并" + # 给 PR 添加评论 gitlink-cli pr +comment --id 3 --body "LGTM, ready to merge" ``` @@ -115,8 +126,11 @@ gitlink-cli api PUT /:owner/:repo/update_file --body '{"filepath":"file.md","con # 检查是否可合并 gitlink-cli api POST /:owner/:repo/pulls/check_can_merge --body '{"head":"dev","base":"main"}' +# 查看 Review 列表 +gitlink-cli api GET /v1/:owner/:repo/pulls/:id/reviews + # 创建 Review -gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{"body":"LGTM","event":"APPROVE"}' +gitlink-cli api POST /v1/:owner/:repo/pulls/:id/reviews --body '{"content":"LGTM","status":"approved"}' # 获取可用分支 gitlink-cli api GET /:owner/:repo/pulls/get_branches @@ -131,6 +145,8 @@ gitlink-cli api GET /:owner/:repo/pulls/get_branches - PR 查看/合并/关闭需要使用 `pull_request_number`(即网页 URL `/pulls/N` 中的序号,从 `pr +list` 返回) - `pr +merge` 默认使用 merge 方式,可通过 `--method` 指定 rebase 或 squash - `pr +diff` 实际调用 `/pulls/:id/files` 端点,返回变更文件列表和 diff 内容 +- `pr +reviews` / `pr +review` 使用 v1 API,`--id` 为网页 URL `/pulls/N` 中的 PR 序号 +- `pr +review --status` 支持 `common`、`approved`、`rejected`;写入前建议先使用 `--dry-run` 预览 - `pr +list` 的 `--state` 参数(open/merged/closed)仅影响统计计数,API 返回的列表可能包含所有状态的 PR - PR 状态值:`pull_request_status` 0=open, 1=merged, 2=closed - 关联已有 Issue 时,把 Issue 编号或 URL 写入 PR `--body`,或使用 `issue +comment` 留痕;不要用 Raw API 对 Issue 做不完整更新,否则可能清空 Issue 描述 diff --git a/skills/gitlink-pr/references/gitlink-pr-review.md b/skills/gitlink-pr/references/gitlink-pr-review.md new file mode 100644 index 0000000..32fb96d --- /dev/null +++ b/skills/gitlink-pr/references/gitlink-pr-review.md @@ -0,0 +1,73 @@ +# pr +review + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +创建 Pull Request 审查,可提交普通审查意见、通过(approve)或拒绝/请求修改(reject)。写入前建议先使用 `--dry-run` 预览请求内容。 + +## 命令 + +```bash +# 预览普通审查评论 +gitlink-cli pr +review --id 3 --status common --content "整体看起来可以" --dry-run + +# 提交普通审查评论 +gitlink-cli pr +review --id 3 --status common --content "整体看起来可以" + +# 通过 PR +gitlink-cli pr +review --id 3 --status approved --content "LGTM" + +# 请求修改 / 拒绝通过 +gitlink-cli pr +review --id 3 --status rejected --content "测试未通过,请修复后再合并" + +# 绑定到指定 commit +gitlink-cli pr +review --id 3 --status approved --content "LGTM" --commit +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--id` / `-i` | 是 | PR 序号(网页 URL `/pulls/N` 中的 `N`,即 `pull_request_number`) | +| `--status` / `-s` | 否 | 审查状态:`common`、`approved`、`rejected`,默认 `common` | +| `--content` / `-c` | 是 | 审查内容 | +| `--commit` / `-m` | 否 | 绑定审查的 commit SHA,对应 API 字段 `commit_id` | +| `--dry-run` | 否 | 只预览请求,不创建审查 | + +## API + +``` +POST /v1/{owner}/{repo}/pulls/{number}/reviews +``` + +请求体: + +```json +{ + "content": "LGTM", + "status": "approved", + "commit_id": "" +} +``` + +其中 `commit_id` 仅在传入 `--commit` 时发送。 + +## 安全流程 + +对写操作建议遵循: + +1. 先用 `pr +view` / `pr +files` / `pr +reviews` 获取上下文。 +2. 准备审查内容和状态。 +3. 执行 `pr +review --dry-run` 预览。 +4. 用户确认后去掉 `--dry-run` 执行真实写入。 + +## 注意事项 + +- `--status approved` 表示通过 PR;`--status rejected` 表示拒绝/请求修改;`--status common` 表示普通审查意见。 +- `--content` 必填,避免产生没有上下文的 approve/reject。 +- `--dry-run` 不会请求 GitLink API,适合 Agent 在执行写操作前展示计划。 + +## References + +- [gitlink-shared SKILL.md](../../gitlink-shared/SKILL.md) -- 认证与全局参数 +- [gitlink-pr SKILL.md](../SKILL.md) -- PR 操作总览 +- [pr +reviews](gitlink-pr-reviews.md) -- 查看 PR 审查记录 diff --git a/skills/gitlink-pr/references/gitlink-pr-reviews.md b/skills/gitlink-pr/references/gitlink-pr-reviews.md new file mode 100644 index 0000000..73c24c1 --- /dev/null +++ b/skills/gitlink-pr/references/gitlink-pr-reviews.md @@ -0,0 +1,61 @@ +# pr +reviews + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +查看 Pull Request 的审查记录,可按审查状态过滤。适合在合并前检查是否已有 approve、reject 或普通审查意见。 + +## 命令 + +```bash +# 查看 PR 审查记录 +gitlink-cli pr +reviews --id 3 + +# 简写 +gitlink-cli pr +reviews -i 3 + +# 按状态筛选 +gitlink-cli pr +reviews -i 3 --status approved +gitlink-cli pr +reviews -i 3 --status rejected +gitlink-cli pr +reviews -i 3 --status common + +# 指定仓库并输出 JSON +gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 3 --format json +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--id` / `-i` | 是 | PR 序号(网页 URL `/pulls/N` 中的 `N`,即 `pull_request_number`) | +| `--status` / `-s` | 否 | 审查状态:`common`、`approved`、`rejected` | + +## API + +``` +GET /v1/{owner}/{repo}/pulls/{number}/reviews +``` + +当传入 `--status` 时,会附加查询参数: + +``` +status= +``` + +## 典型流程 + +1. `pr +view --id ` 确认 PR 状态。 +2. `pr +files --id ` 查看变更文件。 +3. `pr +reviews --id ` 查看现有审查记录。 +4. 根据审查结果决定是否使用 `pr +review` 添加普通评论、approve 或 reject。 + +## 注意事项 + +- `--id` 不是数据库 ID,而是网页 URL `/pulls/N` 中的 PR 序号。 +- 这是只读查询命令,不会修改 PR。 +- `approved` 表示通过,`rejected` 表示拒绝/请求修改,`common` 表示普通审查评论。 + +## References + +- [gitlink-shared SKILL.md](../../gitlink-shared/SKILL.md) -- 认证与全局参数 +- [gitlink-pr SKILL.md](../SKILL.md) -- PR 操作总览 +- [pr +review](gitlink-pr-review.md) -- 创建 PR 审查