forked from Gitlink/gitlink-cli
775 lines
25 KiB
Go
775 lines
25 KiB
Go
package pr
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
func Shortcuts() []*common.Shortcut {
|
||
return []*common.Shortcut{
|
||
newBatchCloseShortcut(),
|
||
newBatchMergeShortcut(),
|
||
// +list:列出当前仓库的合并请求(支持按状态/里程碑/标签/评审人筛选)
|
||
{
|
||
Name: "list",
|
||
Description: "列出合并请求",
|
||
Long: `List pull requests in the current repository.
|
||
|
||
Shows PR number, title, status, author, and other details.
|
||
Use --state to filter by status (open, merged, closed).
|
||
Use --keyword, --milestone, --tag, --reviewer, --assignee for
|
||
additional filtering, and --sort to control ordering.`,
|
||
Example: ` # List open pull requests
|
||
gitlink pr +list
|
||
|
||
# List merged pull requests
|
||
gitlink pr +list --state merged
|
||
|
||
# List closed pull requests with pagination
|
||
gitlink pr +list --state closed --page 2 --limit 10
|
||
|
||
# Search by keyword
|
||
gitlink pr +list -k "feature"
|
||
|
||
# Filter by milestone and assignee
|
||
gitlink pr +list --milestone 3 --assignee 5
|
||
|
||
# Sort by created date
|
||
gitlink pr +list --sort created_on
|
||
|
||
# Sort by updated date descending
|
||
gitlink pr +list --sort updated_on --sort-direction desc`,
|
||
Flags: []common.Flag{
|
||
{Name: "state", Short: "s", Usage: "Filter: open, merged, closed", Default: "open"},
|
||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||
{Name: "keyword", Short: "k", Usage: "Search by keyword"},
|
||
{Name: "milestone", Short: "m", Usage: "Filter by milestone ID"},
|
||
{Name: "tag", Usage: "Filter by tag ID"},
|
||
{Name: "reviewer", Usage: "Filter by reviewer ID"},
|
||
{Name: "assignee", Short: "a", Usage: "Filter by assignee ID"},
|
||
{Name: "sort", Usage: "Sort field (e.g. created_on, updated_on)"},
|
||
{Name: "sort-direction", Usage: "Sort direction: asc or desc"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
q := url.Values{}
|
||
q.Set("page", ctx.Arg("page"))
|
||
q.Set("limit", ctx.Arg("limit"))
|
||
if s := ctx.Arg("state"); s != "" {
|
||
q.Set("state", s)
|
||
}
|
||
// [P2 参数补全] 以下 7 个查询参数原 CLI 未暴露
|
||
if k := ctx.Arg("keyword"); k != "" {
|
||
q.Set("keyword", k) // 关键词搜索
|
||
}
|
||
if m := ctx.Arg("milestone"); m != "" {
|
||
q.Set("version_id", m) // CLI "milestone" → API "version_id"(PR 的里程碑字段名和 issue 不同)
|
||
}
|
||
if t := ctx.Arg("tag"); t != "" {
|
||
q.Set("issue_tag_id", t) // CLI "tag" → API "issue_tag_id"
|
||
}
|
||
if r := ctx.Arg("reviewer"); r != "" {
|
||
q.Set("reviewer_id", r) // CLI "reviewer" → API "reviewer_id"
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
q.Set("assign_user_id", a) // CLI "assignee" → API "assign_user_id"
|
||
}
|
||
if s := ctx.Arg("sort"); s != "" {
|
||
q.Set("sort", s) // 排序字段
|
||
}
|
||
if d := ctx.Arg("sort-direction"); d != "" {
|
||
q.Set("sort_direction", d) // 排序方向
|
||
}
|
||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/pulls", q)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +create:创建新合并请求(需 --title 和 --head 源分支,目标分支默认仓库默认分支)
|
||
{
|
||
Name: "create",
|
||
Description: "创建合并请求",
|
||
Long: `Create a new pull request in the current repository.
|
||
|
||
Requires a title and source branch (--head). The target branch
|
||
(--base) defaults to the repository default branch if not specified.
|
||
Use --assignee, --milestone, --label, and --priority to set
|
||
additional fields at creation time.`,
|
||
Example: ` # Create a pull request
|
||
gitlink pr +create --title "Add new feature" --head feature-branch
|
||
|
||
# Create a PR with description and target branch
|
||
gitlink pr +create --title "Fix bug" --body "Fixes #123" --head fix-branch --base main
|
||
|
||
# Create with assignee and milestone
|
||
gitlink pr +create --title "Feature" --head feat --assignee 5 --milestone 3
|
||
|
||
# Create with labels and priority
|
||
gitlink pr +create --title "Feature" --head feat --label 7 --priority 1`,
|
||
Flags: []common.Flag{
|
||
{Name: "title", Short: "t", Usage: "PR title", Required: true},
|
||
{Name: "body", Short: "b", Usage: "PR description"},
|
||
{Name: "head", Usage: "Source branch", Required: true},
|
||
{Name: "base", Usage: "Target branch (default: repository default branch)"},
|
||
{Name: "priority", Usage: "Priority ID"},
|
||
{Name: "assignee", Short: "a", Usage: "Assignee user ID"},
|
||
{Name: "milestone", Short: "m", Usage: "Milestone ID"},
|
||
{Name: "label", Short: "l", Usage: "Label ID (comma-separated for multiple)"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
title, _ := ctx.RequireArg("title")
|
||
head, _ := ctx.RequireArg("head")
|
||
base := ctx.Arg("base")
|
||
// [P1 Bug#2] 原代码: base = "master"(硬编码)
|
||
// 修复: 使用 GetDefaultBranch() 动态查询仓库默认分支
|
||
if base == "" {
|
||
base = common.GetDefaultBranch(ctx)
|
||
}
|
||
payload := map[string]interface{}{
|
||
"title": title,
|
||
"head": head,
|
||
"base": base,
|
||
}
|
||
if b := ctx.Arg("body"); b != "" {
|
||
payload["body"] = b
|
||
}
|
||
// [P2 参数补全] PR 创建时支持 priority、assignee、milestone、label
|
||
if p := ctx.Arg("priority"); p != "" {
|
||
payload["priority_id"] = p // CLI "priority" → API "priority_id"
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
payload["assigned_to_id"] = a // CLI "assignee" → API "assigned_to_id"
|
||
}
|
||
if m := ctx.Arg("milestone"); m != "" {
|
||
payload["fixed_version_id"] = m // CLI "milestone" → API "fixed_version_id"
|
||
}
|
||
// [P2 参数补全] PR 的标签需要数组格式 issue_tag_ids(和 issue 的单值 issue_tag_id 不同)
|
||
// 支持逗号分隔多标签:--label "1,2,3" → [1, 2, 3]
|
||
if l := ctx.Arg("label"); l != "" {
|
||
if strings.Contains(l, ",") {
|
||
// 多标签:拆分、去空格、转为 []interface{} 数组
|
||
parts := strings.Split(l, ",")
|
||
ids := make([]interface{}, len(parts))
|
||
for i, p := range parts {
|
||
ids[i] = strings.TrimSpace(p)
|
||
}
|
||
payload["issue_tag_ids"] = ids
|
||
} else {
|
||
// 单标签也用数组格式
|
||
payload["issue_tag_ids"] = []interface{}{l}
|
||
}
|
||
}
|
||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls", payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +view:查看指定合并请求的详情(标题/描述/状态/作者/分支等)
|
||
{
|
||
Name: "view",
|
||
Description: "查看合并请求详情",
|
||
Long: `View detailed information about a specific pull request.
|
||
|
||
Displays PR title, description, status, author, branches,
|
||
and other metadata.`,
|
||
Example: ` # View details of pull request #42
|
||
gitlink pr +view --id 42`,
|
||
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, _ := ctx.RequireArg("id")
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +merge:合并合并请求(支持 merge/rebase/squash 三种方式,可自定义提交信息)
|
||
{
|
||
Name: "merge",
|
||
Description: "合并合并请求",
|
||
Long: `Merge a pull request using the specified merge method.
|
||
|
||
Supported merge methods: merge (default), rebase, squash.
|
||
Use --title and --body to customize the merge commit message.`,
|
||
Example: ` # Merge a pull request using the default method
|
||
gitlink pr +merge --id 42
|
||
|
||
# Squash-merge a pull request
|
||
gitlink pr +merge --id 42 --method squash
|
||
|
||
# Rebase-merge a pull request
|
||
gitlink pr +merge --id 42 --method rebase
|
||
|
||
# Merge with custom commit title and description
|
||
gitlink pr +merge --id 42 --title "Merge feature X" --body "Combines feature X into main"`,
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "method", Short: "m", Usage: "Merge method: merge, rebase, squash", Default: "merge"},
|
||
{Name: "title", Usage: "Merge commit title"},
|
||
{Name: "body", Usage: "Merge commit description"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
id, _ := ctx.RequireArg("id")
|
||
method := ctx.Arg("method")
|
||
if method == "" {
|
||
method = "merge"
|
||
}
|
||
if err := validateMergeMethod(method); err != nil {
|
||
return err
|
||
}
|
||
payload := map[string]interface{}{
|
||
"do": method,
|
||
}
|
||
if t := ctx.Arg("title"); t != "" {
|
||
payload["title"] = t
|
||
}
|
||
if b := ctx.Arg("body"); b != "" {
|
||
payload["body"] = b
|
||
}
|
||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/pr_merge", ctx.RepoPath(), id), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +close:关闭合并请求(不合并,关闭后需重新开启才能合并)
|
||
{
|
||
Name: "close",
|
||
Description: "关闭合并请求",
|
||
Long: `Close a pull request without merging.
|
||
|
||
The PR will be marked as closed and cannot be merged afterwards
|
||
unless it is reopened.`,
|
||
Example: ` # Close pull request #42
|
||
gitlink pr +close --id 42`,
|
||
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, _ := ctx.RequireArg("id")
|
||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +files:列出合并请求中的变更文件(路径/变更类型/增删行数)
|
||
{
|
||
Name: "files",
|
||
Description: "列出合并请求的变更文件",
|
||
Long: `List all files changed in a pull request.
|
||
|
||
Shows the file path, change type (added, modified, deleted),
|
||
and the number of additions and deletions for each file.`,
|
||
Example: ` # List files changed in pull request #42
|
||
gitlink pr +files --id 42`,
|
||
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, _ := ctx.RequireArg("id")
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +versions:列出合并请求的补丁集版本历史
|
||
{
|
||
Name: "versions",
|
||
Description: "列出合并请求的补丁集版本",
|
||
Long: `List all patchset versions of a pull request.
|
||
|
||
Each time a PR is updated with new commits, a new patchset version
|
||
is created. This command shows the version history.`,
|
||
Example: ` # List patchset versions for pull request #42
|
||
gitlink pr +versions --id 42`,
|
||
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)+"/versions", nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// [P1 Bug#3 修复] 原代码中 +diff 和 +files 调用了同一个 API 端点,功能完全重复
|
||
// 修复: 将 +diff 改名为 +version-diff,指向 PR 补丁集版本的差异 API
|
||
// +version-diff:查看合并请求某补丁集版本的差异(可用 --file 过滤指定文件)
|
||
// +files 仍然用于查看 PR 文件列表,两者功能区分开
|
||
{
|
||
Name: "version-diff",
|
||
Description: "查看合并请求补丁集版本的差异",
|
||
Long: `Show the diff for a specific patchset version of a pull request.
|
||
|
||
Use --file to filter the diff to a specific file path.`,
|
||
Example: ` # Show diff for version 3 of pull request #42
|
||
gitlink pr +version-diff --id 42 --version-id 3
|
||
|
||
# Show diff for a specific file in a patchset version
|
||
gitlink pr +version-diff --id 42 --version-id 3 --file src/main.go`,
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "version-id", Short: "v", Usage: "Patchset version ID", Required: true},
|
||
{Name: "file", Short: "f", Usage: "Filter diff by file path"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
versionID, err := ctx.RequireArg("version-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
path := fmt.Sprintf("%s/versions/%s/diff", prV1Path(ctx, id), versionID)
|
||
if file := ctx.Arg("file"); file != "" {
|
||
q := url.Values{}
|
||
q.Set("filepath", file)
|
||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
}
|
||
env, err := ctx.CallAPI("GET", path, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +reviews:列出合并请求的评审记录(可按评审状态筛选)
|
||
{
|
||
Name: "reviews",
|
||
Description: "列出合并请求的评审",
|
||
Long: `List all reviews submitted for a pull request.
|
||
|
||
Use --status to filter reviews by their status:
|
||
common, approved, or rejected.`,
|
||
Example: ` # List all reviews for pull request #42
|
||
gitlink pr +reviews --id 42
|
||
|
||
# List only approved reviews
|
||
gitlink pr +reviews --id 42 --status approved`,
|
||
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)
|
||
},
|
||
},
|
||
// +review:对合并请求提交评审(common/approved/rejected,支持 --dry-run 预览)
|
||
{
|
||
Name: "review",
|
||
Description: "创建合并请求评审",
|
||
Long: `Submit a review on a pull request.
|
||
|
||
Supported review statuses: common (comment), approved, rejected.
|
||
Use --commit to attach the review to a specific commit.
|
||
Use --dry-run to preview the review without submitting it.`,
|
||
Example: ` # Submit an approving review
|
||
gitlink pr +review --id 42 --status approved --content "Looks good!"
|
||
|
||
# Request changes on a PR
|
||
gitlink pr +review --id 42 --status rejected --content "Needs fixes"
|
||
|
||
# Preview a review without creating it
|
||
gitlink pr +review --id 42 --status common --content "Note" --dry-run`,
|
||
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)
|
||
},
|
||
},
|
||
// +comments:列出合并请求下的评审评论
|
||
{
|
||
Name: "comments",
|
||
Description: "列出合并请求的评审评论",
|
||
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)+"/journals", nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +create-comment:在合并请求下创建一条评审评论(可附行号与文件路径)
|
||
{
|
||
Name: "create-comment",
|
||
Description: "创建合并请求的评审评论",
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "body", Short: "b", Usage: "Comment body", Required: true},
|
||
{Name: "line", Short: "l", Usage: "Line number"},
|
||
{Name: "path", Short: "p", Usage: "File path"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body, err := ctx.RequireArg("body")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
payload := map[string]interface{}{
|
||
"notes": body,
|
||
}
|
||
if line := ctx.Arg("line"); line != "" {
|
||
payload["line"] = line
|
||
}
|
||
if path := ctx.Arg("path"); path != "" {
|
||
payload["path"] = path
|
||
}
|
||
env, err := ctx.CallAPI("POST", prV1Path(ctx, id)+"/journals", payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +update-comment:修改合并请求下的某条评审评论
|
||
{
|
||
Name: "update-comment",
|
||
Description: "修改合并请求的评审评论",
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "comment-id", Short: "c", Usage: "Comment ID", Required: true},
|
||
{Name: "body", Short: "b", Usage: "New comment body", 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
|
||
}
|
||
commentID, err := ctx.RequireArg("comment-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body, err := ctx.RequireArg("body")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
payload := map[string]interface{}{
|
||
"notes": body,
|
||
}
|
||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/journals/%s", prV1Path(ctx, id), commentID), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +delete-comment:删除合并请求下的某条评审评论
|
||
{
|
||
Name: "delete-comment",
|
||
Description: "删除合并请求的评审评论",
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "comment-id", Short: "c", Usage: "Comment ID", 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
|
||
}
|
||
commentID, err := ctx.RequireArg("comment-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/journals/%s", prV1Path(ctx, id), commentID), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +commits:列出合并请求中包含的提交记录
|
||
{
|
||
Name: "commits",
|
||
Description: "列出合并请求的提交",
|
||
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", fmt.Sprintf("%s/pulls/%s/commits", ctx.RepoPath(), id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +reopen:重新打开已关闭的合并请求
|
||
{
|
||
Name: "reopen",
|
||
Description: "重新打开合并请求",
|
||
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("POST", prV1Path(ctx, id)+"/reopen", nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +update:更新合并请求的标题和/或描述
|
||
{
|
||
Name: "update",
|
||
Description: "更新合并请求的标题或描述",
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "title", Short: "t", Usage: "New title"},
|
||
{Name: "body", Short: "b", Usage: "New description"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
title := ctx.Arg("title")
|
||
body := ctx.Arg("body")
|
||
if title == "" && body == "" {
|
||
return fmt.Errorf("at least one of --title or --body is required")
|
||
}
|
||
payload := map[string]interface{}{}
|
||
if title != "" {
|
||
payload["title"] = title
|
||
}
|
||
if body != "" {
|
||
payload["body"] = body
|
||
}
|
||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +comment:在合并请求下添加评论(作为关联疑修的日志条目发布)
|
||
{
|
||
Name: "comment",
|
||
Description: "在合并请求下添加评论",
|
||
Long: `Add a comment to a pull request.
|
||
|
||
The comment is posted as a journal entry on the underlying issue
|
||
associated with the pull request.`,
|
||
Example: ` # Add a comment to pull request #42
|
||
gitlink pr +comment --id 42 --body "This looks great, thanks!"`,
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "body", Short: "b", Usage: "Comment body", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
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
|
||
}
|
||
|
||
payload := map[string]interface{}{
|
||
"notes": body,
|
||
}
|
||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/issues/%d/journals", ctx.Owner, ctx.Repo, issueID), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
// [P2 参数补全] 校验合并方式参数,只允许 merge/rebase/squash 三种合法值
|
||
// 这是纯前端校验,在发送 API 请求之前拦截非法输入
|
||
func validateMergeMethod(method string) error {
|
||
switch method {
|
||
case "merge", "rebase", "squash":
|
||
return nil
|
||
default:
|
||
return fmt.Errorf("invalid --method %q: use merge, rebase, or squash", method)
|
||
}
|
||
}
|
||
|
||
func extractIssueID(env *output.Envelope) (int64, error) {
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
return 0, fmt.Errorf("unexpected PR response format")
|
||
}
|
||
issue, ok := data["issue"].(map[string]interface{})
|
||
if !ok {
|
||
return 0, fmt.Errorf("PR response missing issue field")
|
||
}
|
||
idFloat, ok := issue["id"].(float64)
|
||
if !ok {
|
||
return 0, fmt.Errorf("PR response missing issue.id field")
|
||
}
|
||
return int64(idFloat), nil
|
||
}
|