From 0b48ec90d8c58b9aa5fd4c56821308aacd2ad601 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Fri, 5 Jun 2026 11:43:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20Issue=20=E5=92=8C?= =?UTF-8?q?=20PR=20=E5=88=97=E8=A1=A8=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++ README.zh-CN.md | 6 ++++ doc/changes/list-filter-fixes.md | 12 +++++++ internal/i18n/locales/en-US.json | 12 +++++++ internal/i18n/locales/zh-CN.json | 12 +++++++ shortcuts/issue/issue.go | 52 +++++++++++++++++++++++++-- shortcuts/issue/issue_test.go | 56 +++++++++++++++++++++++++++-- shortcuts/pr/pr.go | 61 +++++++++++++++++++++++++++++--- shortcuts/pr/pr_test.go | 61 ++++++++++++++++++++++++++++++-- skills/gitlink-issue/SKILL.md | 3 ++ skills/gitlink-pr/SKILL.md | 5 ++- 11 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 doc/changes/list-filter-fixes.md diff --git a/README.md b/README.md index 797e6c7..e18d910 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,9 @@ gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role develope # List issues gitlink-cli issue +list --owner Gitlink --repo forgeplus +# Search and sort open issues +gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open --keyword login --sort-by issues.updated_on --sort-direction desc + # Create an issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..." @@ -318,6 +321,9 @@ gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 # List PRs gitlink-cli pr +list --owner Gitlink --repo forgeplus +# Search merged PRs by keyword +gitlink-cli pr +list --owner Gitlink --repo forgeplus --state merged --keyword release --sort-by updated_at --sort-direction desc + # Create a PR (same-repo branch) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: Search feature" --head feature/search --base master diff --git a/README.zh-CN.md b/README.zh-CN.md index 88c6119..ff469be 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -259,6 +259,9 @@ gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role develope # 列出 Issue gitlink-cli issue +list --owner Gitlink --repo forgeplus +# 搜索并按更新时间倒序列出开启中的 Issue +gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open --keyword 登录 --sort-by issues.updated_on --sort-direction desc + # 创建 Issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..." @@ -328,6 +331,9 @@ gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 # 列出 PR gitlink-cli pr +list --owner Gitlink --repo forgeplus +# 按关键字搜索已合并 PR +gitlink-cli pr +list --owner Gitlink --repo forgeplus --state merged --keyword release --sort-by updated_at --sort-direction desc + # 创建 PR(同仓库分支) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 搜索功能" --head feature/search --base master diff --git a/doc/changes/list-filter-fixes.md b/doc/changes/list-filter-fixes.md new file mode 100644 index 0000000..a3f2e15 --- /dev/null +++ b/doc/changes/list-filter-fixes.md @@ -0,0 +1,12 @@ +# Issue/PR 列表筛选增强 + +本次变更修正并增强 `issue +list` 与 `pr +list` 的筛选能力。 + +此前 `issue +list --state open` 会向服务端发送 `state=open`,但 GitLink v1 Issue 列表接口实际使用 `category=opened/closed/all`,因此列表可能仍返回关闭 Issue。`pr +list --state open` 也没有映射到 PR 列表接口实际使用的 `status=0/1/2` 参数,Skill 文档中甚至需要提醒用户该参数可能只影响统计。现在两个命令都会保留原有 `--state` 用户体验,同时转换为服务端真实生效的参数。 + +新增筛选项: + +- `issue +list` 支持 `--keyword`、`--participant`、`--author-id`、`--assignee-id`、`--milestone-id`、`--status-id`、`--tag-ids`、`--sort-by`、`--sort-direction`。 +- `pr +list` 支持 `--keyword`、`--priority-id`、`--tag-id`、`--milestone-id`、`--reviewer-id`、`--assignee-id`、`--sort-by`、`--sort-direction`。 + +单元测试覆盖了状态映射、筛选参数透传和 `all` 状态兼容;README、中文 README、Issue Skill 与 PR Skill 已同步更新。 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index d9f3c34..db570ed 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -112,6 +112,8 @@ "flag.issue.batch_label.state": "Filter by issue state", "flag.issue.batch_list.limit": "Maximum issues to return, capped at 100", "flag.issue.batch_process.limit": "Maximum issues to process, capped at 100", + "flag.issue.assignee_id": "Assignee user ID", + "flag.issue.author_id": "Author user ID", "flag.issue.body": "Issue description", "flag.issue.label": "Label ID", "flag.issue.label_filter": "Filter by existing label", @@ -121,7 +123,10 @@ "flag.issue.new_title": "New title", "flag.issue.number": "Issue number (as shown in the web URL)", "flag.issue.older_than_days": "Only include issues inactive for at least this many days", + "flag.issue.participant": "Participant filter: all, aboutme, authoredme, assignedme, atme", "flag.issue.state": "Filter by state: open, closed, all", + "flag.issue.status_id": "Issue status ID", + "flag.issue.tag_ids": "Comma-separated issue tag IDs", "flag.issue.title": "Issue title", "flag.lang": "Display language", "flag.limit": "Items per page", @@ -130,17 +135,22 @@ "flag.org.name": "Organization name", "flag.owner": "Repository owner (auto-detected from git remote)", "flag.page": "Page number", + "flag.pr.assignee_id": "Assignee user ID", "flag.pr.base": "Target branch", "flag.pr.body": "PR description", "flag.pr.file": "Filter diff by file path", "flag.pr.head": "Source branch", "flag.pr.id": "PR number", + "flag.pr.milestone_id": "Milestone ID", "flag.pr.merge_method": "Merge method: merge, rebase, squash", + "flag.pr.priority_id": "Priority ID", "flag.pr.review_commit": "Commit SHA to attach the review to", "flag.pr.review_content": "Review content", + "flag.pr.reviewer_id": "Reviewer user ID", "flag.pr.review_status": "Review status: common, approved, rejected", "flag.pr.review_status_filter": "Filter review status: common, approved, rejected", "flag.pr.state": "Filter: open, merged, closed", + "flag.pr.tag_id": "Issue tag ID", "flag.pr.title": "PR title", "flag.pr.version_id": "Patchset version ID", "flag.release.body": "Release notes", @@ -156,6 +166,8 @@ "flag.repo.name": "Repository name", "flag.repo.private": "Make repository private (true/false)", "flag.search.keyword": "Search keyword", + "flag.sort_by": "Sort field", + "flag.sort_direction": "Sort direction: asc, desc", "flag.user": "User login (default: current user)", "flag.user.login": "User login name", "flag.webhook.active": "Whether the webhook is active: true or false", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 848e605..5484e3c 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -112,6 +112,8 @@ "flag.issue.batch_label.state": "按议题状态筛选", "flag.issue.batch_list.limit": "最多返回的议题数,上限 100", "flag.issue.batch_process.limit": "最多处理的议题数,上限 100", + "flag.issue.assignee_id": "负责人用户 ID", + "flag.issue.author_id": "作者用户 ID", "flag.issue.body": "议题描述", "flag.issue.label": "标签 ID", "flag.issue.label_filter": "按已有标签筛选", @@ -121,7 +123,10 @@ "flag.issue.new_title": "新标题", "flag.issue.number": "议题编号(网页 URL 中显示的编号)", "flag.issue.older_than_days": "只包含至少这么多天未活动的议题", + "flag.issue.participant": "参与范围:all、aboutme、authoredme、assignedme、atme", "flag.issue.state": "按状态筛选:open、closed、all", + "flag.issue.status_id": "议题状态 ID", + "flag.issue.tag_ids": "逗号分隔的议题标签 ID", "flag.issue.title": "议题标题", "flag.lang": "显示语言", "flag.limit": "每页条目数", @@ -130,17 +135,22 @@ "flag.org.name": "组织名称", "flag.owner": "仓库所有者(自动从 git remote 检测)", "flag.page": "页码", + "flag.pr.assignee_id": "指派人用户 ID", "flag.pr.base": "目标分支", "flag.pr.body": "PR 描述", "flag.pr.file": "按文件路径筛选 diff", "flag.pr.head": "源分支", "flag.pr.id": "PR 编号", + "flag.pr.milestone_id": "里程碑 ID", "flag.pr.merge_method": "合并方式:merge、rebase、squash", + "flag.pr.priority_id": "优先级 ID", "flag.pr.review_commit": "关联评审的 Commit SHA", "flag.pr.review_content": "评审内容", + "flag.pr.reviewer_id": "评审人用户 ID", "flag.pr.review_status": "评审状态:common、approved、rejected", "flag.pr.review_status_filter": "按评审状态筛选:common、approved、rejected", "flag.pr.state": "筛选:open、merged、closed", + "flag.pr.tag_id": "议题标签 ID", "flag.pr.title": "PR 标题", "flag.pr.version_id": "补丁集版本 ID", "flag.release.body": "发布说明", @@ -156,6 +166,8 @@ "flag.repo.name": "仓库名称", "flag.repo.private": "设为私有仓库(true/false)", "flag.search.keyword": "搜索关键词", + "flag.sort_by": "排序字段", + "flag.sort_direction": "排序方向:asc、desc", "flag.user": "用户登录名(默认:当前用户)", "flag.user.login": "用户登录名", "flag.webhook.active": "Webhook 是否启用:true 或 false", diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index f4d4df4..9e5a117 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -16,6 +16,19 @@ func v1RepoPath(ctx *common.RuntimeContext) string { return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo) } +func normalizeIssueListState(state string) string { + switch strings.ToLower(strings.TrimSpace(state)) { + case "open", "opened": + return "opened" + case "closed": + return "closed" + case "all", "": + return "all" + default: + return state + } +} + type existingIssue struct { Subject string Description string @@ -37,6 +50,15 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { Description: tr.T("cmd.issue.list.short"), Flags: []common.Flag{ {Name: "state", Short: "s", Usage: tr.T("flag.issue.state"), Default: "open"}, + {Name: "keyword", Short: "k", Usage: tr.T("flag.search.keyword")}, + {Name: "participant", Usage: tr.T("flag.issue.participant")}, + {Name: "author-id", Usage: tr.T("flag.issue.author_id")}, + {Name: "assignee-id", Usage: tr.T("flag.issue.assignee_id")}, + {Name: "milestone-id", Usage: tr.T("flag.issue.milestone")}, + {Name: "status-id", Usage: tr.T("flag.issue.status_id")}, + {Name: "tag-ids", Usage: tr.T("flag.issue.tag_ids")}, + {Name: "sort-by", Usage: tr.T("flag.sort_by")}, + {Name: "sort-direction", Usage: tr.T("flag.sort_direction")}, {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, @@ -48,7 +70,34 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { q.Set("page", ctx.Arg("page")) q.Set("limit", ctx.Arg("limit")) if s := ctx.Arg("state"); s != "" { - q.Set("state", s) + q.Set("category", normalizeIssueListState(s)) + } + if keyword := ctx.Arg("keyword"); keyword != "" { + q.Set("keyword", keyword) + } + if participant := ctx.Arg("participant"); participant != "" { + q.Set("participant_category", participant) + } + if authorID := ctx.Arg("author-id"); authorID != "" { + q.Set("author_id", authorID) + } + if assigneeID := ctx.Arg("assignee-id"); assigneeID != "" { + q.Set("assigner_id", assigneeID) + } + if milestoneID := ctx.Arg("milestone-id"); milestoneID != "" { + q.Set("milestone_id", milestoneID) + } + if statusID := ctx.Arg("status-id"); statusID != "" { + q.Set("status_id", statusID) + } + if tagIDs := ctx.Arg("tag-ids"); tagIDs != "" { + q.Set("issue_tag_ids", tagIDs) + } + if sortBy := ctx.Arg("sort-by"); sortBy != "" { + q.Set("sort_by", sortBy) + } + if sortDirection := ctx.Arg("sort-direction"); sortDirection != "" { + q.Set("sort_direction", sortDirection) } env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q) if err != nil { @@ -599,4 +648,3 @@ func parseIssueID(value, flagName string) (int, error) { } return id, nil } - diff --git a/shortcuts/issue/issue_test.go b/shortcuts/issue/issue_test.go index e9e3389..48be057 100644 --- a/shortcuts/issue/issue_test.go +++ b/shortcuts/issue/issue_test.go @@ -104,8 +104,8 @@ func TestIssueList(t *testing.T) { if r.URL.Path != "/v1/owner/repo/issues.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - if r.URL.Query().Get("state") != "open" { - t.Fatalf("expected state=open, got %s", r.URL.Query().Get("state")) + if r.URL.Query().Get("category") != "opened" { + t.Fatalf("expected category=opened, got %s", r.URL.Query().Get("category")) } writeJSON(t, w, []interface{}{ map[string]interface{}{"id": float64(1), "subject": "bug"}, @@ -119,6 +119,58 @@ func TestIssueList(t *testing.T) { } } +func TestIssueListWithFilters(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/owner/repo/issues.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + query := r.URL.Query() + assertEqual(t, query.Get("category"), "closed") + assertEqual(t, query.Get("keyword"), "release") + assertEqual(t, query.Get("participant_category"), "assignedme") + assertEqual(t, query.Get("author_id"), "10") + assertEqual(t, query.Get("assigner_id"), "11") + assertEqual(t, query.Get("milestone_id"), "12") + assertEqual(t, query.Get("status_id"), "5") + assertEqual(t, query.Get("issue_tag_ids"), "1,2") + assertEqual(t, query.Get("sort_by"), "issues.updated_on") + assertEqual(t, query.Get("sort_direction"), "desc") + writeJSON(t, w, map[string]interface{}{"issues": []interface{}{}}) + }) + defer server.Close() + + err := runShortcut(t, server, "list", map[string]string{ + "state": "closed", + "keyword": "release", + "participant": "assignedme", + "author-id": "10", + "assignee-id": "11", + "milestone-id": "12", + "status-id": "5", + "tag-ids": "1,2", + "sort-by": "issues.updated_on", + "sort-direction": "desc", + "page": "2", + "limit": "50", + }) + if err != nil { + t.Fatalf("list with filters failed: %v", err) + } +} + +func TestIssueListStateAll(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertEqual(t, r.URL.Query().Get("category"), "all") + writeJSON(t, w, map[string]interface{}{"issues": []interface{}{}}) + }) + defer server.Close() + + err := runShortcut(t, server, "list", map[string]string{"state": "all", "page": "1", "limit": "20"}) + if err != nil { + t.Fatalf("list all failed: %v", err) + } +} + // --- create --- func TestIssueCreate(t *testing.T) { diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 6374499..03f537f 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -10,6 +10,25 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) +func v1RepoPath(ctx *common.RuntimeContext) string { + return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo) +} + +func normalizePullRequestListState(state string) string { + switch strings.ToLower(strings.TrimSpace(state)) { + case "open", "opened": + return "0" + case "merged": + return "1" + case "closed": + return "2" + case "all", "": + return "" + default: + return state + } +} + func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { tr := shortcutTranslator(translators...) return []*common.Shortcut{ @@ -18,6 +37,14 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { Description: tr.T("cmd.pr.list.short"), 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: "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")}, + {Name: "reviewer-id", Usage: tr.T("flag.pr.reviewer_id")}, + {Name: "assignee-id", Usage: tr.T("flag.pr.assignee_id")}, + {Name: "sort-by", Usage: tr.T("flag.sort_by")}, + {Name: "sort-direction", Usage: tr.T("flag.sort_direction")}, {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, @@ -28,10 +55,34 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { 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) + if s := normalizePullRequestListState(ctx.Arg("state")); s != "" { + q.Set("status", s) } - env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/pulls", q) + if keyword := ctx.Arg("keyword"); keyword != "" { + q.Set("keyword", keyword) + } + if priorityID := ctx.Arg("priority-id"); priorityID != "" { + q.Set("priority_id", priorityID) + } + if tagID := ctx.Arg("tag-id"); tagID != "" { + q.Set("issue_tag_id", tagID) + } + if milestoneID := ctx.Arg("milestone-id"); milestoneID != "" { + q.Set("version_id", milestoneID) + } + if reviewerID := ctx.Arg("reviewer-id"); reviewerID != "" { + q.Set("reviewer_id", reviewerID) + } + if assigneeID := ctx.Arg("assignee-id"); assigneeID != "" { + q.Set("assign_user_id", assigneeID) + } + if sortBy := ctx.Arg("sort-by"); sortBy != "" { + q.Set("sort_by", sortBy) + } + if sortDirection := ctx.Arg("sort-direction"); sortDirection != "" { + q.Set("sort_direction", sortDirection) + } + env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/pulls", q) if err != nil { return err } @@ -120,8 +171,8 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { }, }, { - Name: "refuse", - Description: "Refuse and close a pull request", + Name: "refuse", + Description: "Refuse and close a pull request", Flags: []common.Flag{ {Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true}, }, diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index 0eec121..eece6d9 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -99,11 +99,11 @@ func TestPRList(t *testing.T) { if r.Method != "GET" { t.Fatalf("expected GET, got %s", r.Method) } - if r.URL.Path != "/owner/repo/pulls.json" { + if r.URL.Path != "/v1/owner/repo/pulls.json" { t.Fatalf("unexpected path: %s", r.URL.Path) } - if r.URL.Query().Get("state") != "open" { - t.Fatalf("expected state=open, got %s", r.URL.Query().Get("state")) + if r.URL.Query().Get("status") != "0" { + t.Fatalf("expected status=0, got %s", r.URL.Query().Get("status")) } if r.URL.Query().Get("page") != "1" { t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page")) @@ -120,6 +120,61 @@ func TestPRList(t *testing.T) { } } +func TestPRListWithFilters(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/owner/repo/pulls.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + query := r.URL.Query() + assertEqual(t, query.Get("status"), "1") + assertEqual(t, query.Get("keyword"), "release") + assertEqual(t, query.Get("priority_id"), "2") + assertEqual(t, query.Get("issue_tag_id"), "3") + assertEqual(t, query.Get("version_id"), "4") + assertEqual(t, query.Get("reviewer_id"), "5") + assertEqual(t, query.Get("assign_user_id"), "6") + assertEqual(t, query.Get("sort_by"), "updated_at") + assertEqual(t, query.Get("sort_direction"), "desc") + writeJSON(t, w, map[string]interface{}{"pulls": []interface{}{}}) + })) + defer server.Close() + + err := runPRShortcut(t, server, "list", map[string]string{ + "state": "merged", + "keyword": "release", + "priority-id": "2", + "tag-id": "3", + "milestone-id": "4", + "reviewer-id": "5", + "assignee-id": "6", + "sort-by": "updated_at", + "sort-direction": "desc", + "page": "2", + "limit": "50", + }) + if err != nil { + t.Fatalf("list with filters failed: %v", err) + } +} + +func TestPRListStateAllOmitsStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/owner/repo/pulls.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("status"); got != "" { + t.Fatalf("status should be omitted for all, got %q", got) + } + writeJSON(t, w, map[string]interface{}{"pulls": []interface{}{}}) + })) + defer server.Close() + + err := runPRShortcut(t, server, "list", map[string]string{"state": "all", "page": "1", "limit": "20"}) + if err != nil { + t.Fatalf("list all failed: %v", err) + } +} + // --- create --- func TestPRCreate(t *testing.T) { diff --git a/skills/gitlink-issue/SKILL.md b/skills/gitlink-issue/SKILL.md index 5688853..c5a4aa2 100644 --- a/skills/gitlink-issue/SKILL.md +++ b/skills/gitlink-issue/SKILL.md @@ -39,6 +39,9 @@ metadata: # 列出 Issue gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open +# 搜索并排序 Issue +gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open --keyword 登录 --sort-by issues.updated_on --sort-direction desc + # 创建 Issue gitlink-cli issue +create --owner myuser --repo myrepo --title "Bug: 登录失败" --body "复现步骤:..." diff --git a/skills/gitlink-pr/SKILL.md b/skills/gitlink-pr/SKILL.md index 767fb4c..d00091d 100644 --- a/skills/gitlink-pr/SKILL.md +++ b/skills/gitlink-pr/SKILL.md @@ -39,6 +39,9 @@ metadata: # 列出 PR gitlink-cli pr +list --owner Gitlink --repo forgeplus --state open +# 搜索并排序 PR +gitlink-cli pr +list --owner Gitlink --repo forgeplus --state merged --keyword release --sort-by updated_at --sort-direction desc + # 创建 PR(源分支必须有实际代码变更) gitlink-cli pr +create --title "feat: 新增搜索功能" --head feature/search --base master --body "实现了全文搜索" @@ -166,6 +169,6 @@ gitlink-cli api GET /v1/:owner/:repo/pulls/:id/versions/:version_id/diff - `pr +diff` 实际调用 `/pulls/:id/files` 端点,返回变更文件列表和 diff 内容 - `pr +versions` / `pr +version-diff` 使用 v1 API,`--id` 为网页 URL `/pulls/N` 中的 PR 序号,`--version-id` 为 patchset/version id - 同一个 PR 分支继续 push 新 commit 会生成新的 patchset/version;正常根据 review 修改代码时,应优先在原 PR 分支继续 push,不要关闭 PR 重开 -- `pr +list` 的 `--state` 参数(open/merged/closed)仅影响统计计数,API 返回的列表可能包含所有状态的 PR +- `pr +list` 的 `--state` 参数会映射为 GitLink API 的 `status` 筛选;需要查看所有 PR 时传 `--state all` - PR 状态值:`pull_request_status` 0=open, 1=merged, 2=closed - 关联已有 Issue 时,把 Issue 编号或 URL 写入 PR `--body`,或使用 `issue +comment` 留痕;不要用 Raw API 对 Issue 做不完整更新,否则可能清空 Issue 描述