Merge PR #26: feat(pr): add review shortcuts
Resolved conflicts between PR #24 (+versions, +version-diff) and PR #26 (+reviews, +review). Unified prReviewsPath() to use existing prV1Path() helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
49cd590227
|
|
@ -209,6 +209,13 @@ gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
|||
|
||||
# View a patchset version diff
|
||||
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
|
||||
|
||||
# List PR reviews
|
||||
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# Create a PR review (with dry-run preview)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -220,6 +220,13 @@ gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
|||
|
||||
# 查看指定 patchset/version diff
|
||||
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
|
||||
|
||||
# 查看 PR 审查记录
|
||||
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 创建 PR 审查(支持 dry-run 预览)
|
||||
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"
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
|
|
|||
|
|
@ -225,6 +225,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", prV1Path(ctx, id)+"/reviews", 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", prV1Path(ctx, id)+"/reviews", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "comment",
|
||||
Description: "Add a comment to a pull request",
|
||||
|
|
@ -265,6 +346,15 @@ func prV1Path(ctx *common.RuntimeContext, id string) string {
|
|||
return fmt.Sprintf("/v1/%s/%s/pulls/%s", 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 {
|
||||
|
|
|
|||
|
|
@ -164,6 +164,105 @@ func TestPRVersionDiffRequiresVersionID(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)
|
||||
|
|
|
|||
|
|
@ -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 +versions`, `pr +version-diff` |
|
||||
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
|
||||
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` |
|
||||
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` |
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ metadata:
|
|||
| `pr +diff` | 查看变更文件和 diff 内容 | 否 |
|
||||
| `pr +versions` | 查看 PR patchset/version 列表 | 否 |
|
||||
| `pr +version-diff` | 查看指定 patchset/version diff | 否 |
|
||||
| `pr +reviews` | 查看 PR 审查记录,支持状态过滤 | 否 |
|
||||
| `pr +review` | 创建 PR 审查(支持 dry-run 预览) | 是 |
|
||||
| `pr +comment` | 给 PR 添加评论 | 是 |
|
||||
|
||||
## 使用示例
|
||||
|
|
@ -59,6 +61,14 @@ gitlink-cli pr +versions --id 3
|
|||
gitlink-cli pr +version-diff --id 3 --version-id 16040
|
||||
gitlink-cli pr +version-diff --id 3 --version-id 16040 --file shortcuts/pr/pr.go
|
||||
|
||||
# 查看 PR 审查记录(支持按状态过滤)
|
||||
gitlink-cli pr +reviews --id 3
|
||||
gitlink-cli pr +reviews --id 3 --status approved
|
||||
|
||||
# 创建 PR 审查(common/approved/rejected,支持 --dry-run 预览)
|
||||
gitlink-cli pr +review --id 3 --status approved --content "LGTM, ready to merge"
|
||||
gitlink-cli pr +review --id 3 --status approved --content "LGTM" --dry-run
|
||||
|
||||
# 给 PR 添加评论
|
||||
gitlink-cli pr +comment --id 3 --body "LGTM, ready to merge"
|
||||
```
|
||||
|
|
@ -125,7 +135,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 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"}'
|
||||
|
||||
# 查看 Review 列表(支持 status 过滤)
|
||||
gitlink-cli api GET /v1/:owner/:repo/pulls/:id/reviews
|
||||
gitlink-cli api GET /v1/:owner/:repo/pulls/:id/reviews?status=approved
|
||||
|
||||
# 获取可用分支
|
||||
gitlink-cli api GET /:owner/:repo/pulls/get_branches
|
||||
|
|
|
|||
|
|
@ -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 <commit_sha>
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--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_sha>"
|
||||
}
|
||||
```
|
||||
|
||||
其中 `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 审查记录
|
||||
|
|
@ -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=<common|approved|rejected>
|
||||
```
|
||||
|
||||
## 典型流程
|
||||
|
||||
1. `pr +view --id <number>` 确认 PR 状态。
|
||||
2. `pr +files --id <number>` 查看变更文件。
|
||||
3. `pr +reviews --id <number>` 查看现有审查记录。
|
||||
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 审查
|
||||
Loading…
Reference in New Issue