feat(issue): add journal and activity shortcuts

This commit is contained in:
NeeNe 2026-07-02 19:16:19 +08:00
parent 71ca2bb683
commit a5b8cb448d
9 changed files with 214 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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 ./...
```

View File

@ -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",

View File

@ -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",

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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"}'
```

View File

@ -52,7 +52,7 @@ gitlink-cli issue +list --state open --owner <owner> --repo <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 <owner> --repo <repo> --number <number> --category comment --page 1 --limit 50 --format json
```
**AI 判断逻辑**
@ -417,7 +417,7 @@ gitlink-cli issue +list --state open --owner <owner> --repo <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 <owner> --repo <repo> --number <number> --category comment --page 1 --limit 50 --format json
# - 有评论 → 最后活动时间 = 最新评论的 created_at
# - 无评论 → 最后活动时间 = updated_at
# 需注意排除skill自动发送的评论内容