From a5b8cb448d5eadf286b8dbf162c2666550a0c1f9 Mon Sep 17 00:00:00 2001 From: NeeNe <26158277@qq.com> Date: Thu, 2 Jul 2026 19:16:19 +0800 Subject: [PATCH] feat(issue): add journal and activity shortcuts --- README.md | 4 + README.zh-CN.md | 4 + doc/changes/issue-journal-shortcuts.md | 63 ++++++++++++++++ internal/i18n/locales/en-US.json | 3 + internal/i18n/locales/zh-CN.json | 3 + shortcuts/issue/issue.go | 45 +++++++++++ shortcuts/issue/issue_test.go | 82 +++++++++++++++++++++ skills/gitlink-issue/SKILL.md | 13 ++-- skills/gitlink-stale-issue-manager/SKILL.md | 4 +- 9 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 doc/changes/issue-journal-shortcuts.md diff --git a/README.md b/README.md index e5e4318..725b99d 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,10 @@ gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 - # Add a comment gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed" +# List issue journals or comment activity +gitlink-cli issue +journals --owner Gitlink --repo forgeplus --number 123 --page 1 --limit 50 +gitlink-cli issue +activity --owner Gitlink --repo forgeplus --number 123 --category comment + # List issue assigners gitlink-cli issue +assigners --owner Gitlink --repo forgeplus diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..2260150 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -367,6 +367,10 @@ gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 - # 添加评论 gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复" +# 列出 Issue 动态记录或评论活动 +gitlink-cli issue +journals --owner Gitlink --repo forgeplus --number 123 --page 1 --limit 50 +gitlink-cli issue +activity --owner Gitlink --repo forgeplus --number 123 --category comment + # 列出 Issue 负责人 gitlink-cli issue +assigners --owner Gitlink --repo forgeplus diff --git a/doc/changes/issue-journal-shortcuts.md b/doc/changes/issue-journal-shortcuts.md new file mode 100644 index 0000000..7cb2396 --- /dev/null +++ b/doc/changes/issue-journal-shortcuts.md @@ -0,0 +1,63 @@ +# Issue journal shortcuts + +## Background + +Issue comments and journal events are useful for stale issue detection, audit +trails, and triage workflows. Those reads were still documented as Raw API calls +against `/v1/:owner/:repo/issues/:number/journals`. + +This change adds first-class read-only shortcuts for issue journals. + +## New shortcuts + +- `issue +journals` lists raw issue journal records. +- `issue +activity` uses the same journal endpoint as an activity-oriented + alias. + +Both commands preserve the existing issue number convention: + +- `--number` / `-n` is the preferred web-visible issue number. +- `--id` / `-i` remains a compatibility alias for the same web-visible number, + not the database ID. + +## Options + +- `--category` filters journal category, for example `comment`. +- `--page` defaults to `1`. +- `--limit` defaults to `50`. + +## Examples + +```bash +gitlink-cli issue +journals --owner Gitlink --repo forgeplus --number 123 --page 1 --limit 50 +gitlink-cli issue +journals --owner Gitlink --repo forgeplus --number 123 --category comment +gitlink-cli issue +activity --owner Gitlink --repo forgeplus --number 123 --category comment +``` + +## Documentation updates + +- README and README.zh-CN include journal and activity examples. +- `skills/gitlink-issue` documents the new read-only shortcuts. +- `skills/gitlink-stale-issue-manager` now uses `issue +journals` instead of + Raw API for comment history lookup. + +## Tests + +Unit tests cover: + +- endpoint path and query mapping; +- `--number` and `--id` alias behavior; +- missing issue number validation; +- HTTP error propagation. + +Suggested verification: + +```bash +go test ./shortcuts/issue ./shortcuts +``` + +Full project verification: + +```bash +go test ./... +``` diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..d152eb3 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -34,6 +34,7 @@ "cmd.dataset.view.short": "View a repository's dataset", "cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.", "cmd.doctor.short": "Diagnose gitlink-cli environment problems", + "cmd.issue.activity.short": "List issue activity events", "cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", "cmd.issue.batch_close.short": "Close filtered issues in bulk. Defaults to dry-run; pass --yes to execute.", "cmd.issue.batch_label.long": "Add a label to filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote label operations. The current implementation does not fake label writes when the API endpoint is unavailable.\n\nExamples:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes", @@ -43,6 +44,7 @@ "cmd.issue.close.short": "Close an issue", "cmd.issue.comment.short": "Add a comment to an issue", "cmd.issue.create.short": "Create a new issue", + "cmd.issue.journals.short": "List issue journals", "cmd.issue.list.short": "List issues", "cmd.issue.short": "Issue operations", "cmd.issue.update.short": "Update an issue", @@ -158,6 +160,7 @@ "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.body": "Issue description", + "flag.issue.journal_category": "Journal category filter, for example: comment", "flag.issue.label": "Label ID", "flag.issue.label_filter": "Filter by existing label", "flag.issue.milestone": "Milestone ID", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..56c5cf5 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -34,6 +34,7 @@ "cmd.dataset.view.short": "查看仓库数据集", "cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。", "cmd.doctor.short": "诊断 gitlink-cli 环境问题", + "cmd.issue.activity.short": "列出议题活动事件", "cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", "cmd.issue.batch_close.short": "批量关闭筛选后的议题。默认 dry-run;传入 --yes 后执行。", "cmd.issue.batch_label.long": "给筛选后的议题批量添加标签。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端标签操作。当前实现不会在 API 端点不可用时伪造写入结果。\n\n示例:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes", @@ -43,6 +44,7 @@ "cmd.issue.close.short": "关闭议题", "cmd.issue.comment.short": "给议题添加评论", "cmd.issue.create.short": "创建新议题", + "cmd.issue.journals.short": "列出议题动态记录", "cmd.issue.list.short": "列出议题", "cmd.issue.short": "议题操作", "cmd.issue.update.short": "更新议题", @@ -158,6 +160,7 @@ "flag.issue.batch_list.limit": "最多返回的议题数,上限 100", "flag.issue.batch_process.limit": "最多处理的议题数,上限 100", "flag.issue.body": "议题描述", + "flag.issue.journal_category": "动态记录分类筛选,例如:comment", "flag.issue.label": "标签 ID", "flag.issue.label_filter": "按已有标签筛选", "flag.issue.milestone": "里程碑 ID", diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index b19027e..0430652 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -297,6 +297,18 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "journals", + Description: tr.T("cmd.issue.journals.short"), + Flags: issueJournalFlags(tr), + Run: runIssueJournals, + }, + { + Name: "activity", + Description: tr.T("cmd.issue.activity.short"), + Flags: issueJournalFlags(tr), + Run: runIssueJournals, + }, { Name: "assigners", Description: "List issue assigners", @@ -435,6 +447,14 @@ func appendIssueNumberFlags(flags ...common.Flag) []common.Flag { return append(issueNumberFlags(), flags...) } +func issueJournalFlags(tr *i18n.Translator) []common.Flag { + return appendIssueNumberFlags( + common.Flag{Name: "category", Short: "c", Usage: tr.T("flag.issue.journal_category")}, + common.Flag{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + common.Flag{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "50"}, + ) +} + func issueNumberArg(ctx *common.RuntimeContext) (string, error) { if number := strings.TrimSpace(ctx.Arg("number")); number != "" { return number, nil @@ -445,6 +465,25 @@ func issueNumberArg(ctx *common.RuntimeContext) (string, error) { return "", fmt.Errorf("required flag --number is missing (or use --id as a compatibility alias)") } +func runIssueJournals(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + number, err := issueNumberArg(ctx) + if err != nil { + return err + } + q := url.Values{} + setIssueQueryIfPresent(q, "category", ctx.Arg("category")) + setIssueQueryIfPresent(q, "page", ctx.Arg("page")) + setIssueQueryIfPresent(q, "limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q) + if err != nil { + return err + } + return ctx.Output(env) +} + // normalizeIssueListIDs adds "number" (project_issues_index) and renames // "id" to "database_id" so the user-facing output uses the project-level // issue number, not the global database primary key. @@ -650,3 +689,9 @@ func parseIssueID(value, flagName string) (int, error) { } return id, nil } + +func setIssueQueryIfPresent(q url.Values, key, value string) { + if value := strings.TrimSpace(value); value != "" { + q.Set(key, value) + } +} diff --git a/shortcuts/issue/issue_test.go b/shortcuts/issue/issue_test.go index 48be057..96c6c0b 100644 --- a/shortcuts/issue/issue_test.go +++ b/shortcuts/issue/issue_test.go @@ -729,6 +729,74 @@ func TestIssueCommentMissingBody(t *testing.T) { } } +// --- journals / activity --- + +func TestIssueJournals(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/v1/owner/repo/issues/42/journals.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + assertEqual(t, r.URL.Query().Get("category"), "comment") + assertEqual(t, r.URL.Query().Get("page"), "2") + assertEqual(t, r.URL.Query().Get("limit"), "50") + writeJSON(t, w, map[string]interface{}{ + "journals": []interface{}{ + map[string]interface{}{"id": float64(1), "notes": "hello"}, + }, + }) + }) + defer server.Close() + + err := runShortcut(t, server, "journals", map[string]string{ + "number": "42", + "category": "comment", + "page": "2", + "limit": "50", + }) + if err != nil { + t.Fatalf("journals failed: %v", err) + } +} + +func TestIssueActivityUsesSameJournalEndpoint(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/42/journals.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + if got := r.URL.Query().Get("category"); got != "" { + t.Fatalf("category should be omitted when not passed, got %q", got) + } + assertEqual(t, r.URL.Query().Get("page"), "1") + assertEqual(t, r.URL.Query().Get("limit"), "20") + writeJSON(t, w, map[string]interface{}{"journals": []interface{}{}}) + }) + defer server.Close() + + err := runShortcut(t, server, "activity", map[string]string{ + "id": "42", + "page": "1", + "limit": "20", + }) + if err != nil { + t.Fatalf("activity failed: %v", err) + } +} + +func TestIssueJournalsMissingNumber(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runShortcut(t, server, "journals", map[string]string{"category": "comment"}) + if err == nil { + t.Fatal("expected error for missing number") + } +} + func TestIssueNumberOrIDIsRequired(t *testing.T) { server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) @@ -743,6 +811,8 @@ func TestIssueNumberOrIDIsRequired(t *testing.T) { {name: "close", args: map[string]string{}}, {name: "update", args: map[string]string{"title": "New title"}}, {name: "comment", args: map[string]string{"body": "Fixed"}}, + {name: "journals", args: map[string]string{}}, + {name: "activity", args: map[string]string{}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -1038,6 +1108,18 @@ func TestIssueUpdateHTTPError(t *testing.T) { } } +func TestIssueJournalsHTTPError(t *testing.T) { + server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeText(t, w, http.StatusInternalServerError, "server error") + }) + defer server.Close() + + err := runShortcut(t, server, "journals", map[string]string{"number": "42"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + func TestIssueCloseHTTPError(t *testing.T) { server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) { switch { diff --git a/skills/gitlink-issue/SKILL.md b/skills/gitlink-issue/SKILL.md index 9bdb1b3..9f0259b 100644 --- a/skills/gitlink-issue/SKILL.md +++ b/skills/gitlink-issue/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-issue -version: 2.0.0 -description: "Issue 管理:创建、查看、更新、关闭/批量关闭/批量更新/批量删除 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。" +version: 3.0.0 +description: "Issue 管理:创建、查看、更新、关闭/批量关闭/批量更新/批量删除 Issue,添加评论,查看动态记录和活动。当用户需要操作 GitLink Issue 时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -29,6 +29,8 @@ metadata: | `issue +batch-update` | 按 API issue id 批量更新状态、优先级、里程碑、标签、负责人 | 是(dry-run 不写入) | | `issue +batch-delete` | 按 API issue id 批量删除 Issue;真实删除必须 `--yes` | 是(dry-run 不写入) | | `issue +comment` | 添加评论 | 是 | +| `issue +journals` | 查询 Issue 动态记录,支持分类和分页 | 否(公开项目) | +| `issue +activity` | 查询 Issue 活动事件,复用 journals 端点 | 否(公开项目) | | `issue +assigners` | 查询 Issue 负责人列表 | 否(公开项目) | | `issue +authors` | 查询 Issue 发布人列表 | 否(公开项目) | | `issue +statuses` | 查询 Issue 状态列表 | 否(公开项目) | @@ -72,6 +74,10 @@ gitlink-cli issue +batch-delete --owner myuser --repo myrepo --ids 101,102 --yes # 添加评论 gitlink-cli issue +comment --number 4 --body "已修复,请验证" +# 查询 Issue 评论和活动记录 +gitlink-cli issue +journals --number 4 --category comment --page 1 --limit 50 +gitlink-cli issue +activity --number 4 --page 1 --limit 50 + # 查询 Issue 负责人 gitlink-cli issue +assigners --owner Gitlink --repo forgeplus --keyword alice @@ -90,9 +96,6 @@ gitlink-cli issue +authors --owner Gitlink --repo forgeplus --keyword bob ## Raw API 补充 ```bash -# 获取 Issue 评论列表(使用 v1 API,按 issue number 查询) -gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals - # 批量更新 Issue(仍使用旧版 API,需传数据库 ID) gitlink-cli api POST /:owner/:repo/issues/series_update --body '{"ids":[1,2,3],"status_id":"closed"}' ``` diff --git a/skills/gitlink-stale-issue-manager/SKILL.md b/skills/gitlink-stale-issue-manager/SKILL.md index 431a6f1..acd1645 100644 --- a/skills/gitlink-stale-issue-manager/SKILL.md +++ b/skills/gitlink-stale-issue-manager/SKILL.md @@ -52,7 +52,7 @@ gitlink-cli issue +list --state open --owner --repo --page 1 --li ```bash # 获取某个 Issue 的评论列表(如果数据量过大,可能需要翻到最后一页取最后十条) -gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals?category=comment&page=1&limit=50 --format json +gitlink-cli issue +journals --owner --repo --number --category comment --page 1 --limit 50 --format json ``` **AI 判断逻辑**: @@ -417,7 +417,7 @@ gitlink-cli issue +list --state open --owner --repo --format json # Step 2:判断 Issue 最后活动时间 # a. 如果 created_at == updated_at → 最后活动时间 = created_at(无需查询评论) # b. 如果 created_at != updated_at → 查询评论列表获取最新评论时间 -gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals --format json +gitlink-cli issue +journals --owner --repo --number --category comment --page 1 --limit 50 --format json # - 有评论 → 最后活动时间 = 最新评论的 created_at # - 无评论 → 最后活动时间 = updated_at # 需注意排除skill自动发送的评论内容