本次实现:search +issues — 搜索 Issue 功能

新增功能参数
gitlink-cli search +issues --keyword "登录失败"    # 必填:搜索关键词
                           --category opened       # 可选:all/opened/closed
                           --assignee 42           # 可选:按负责人 ID
                           --author 10             # 可选:按创建人 ID
                           --milestone 5           # 可选:按里程碑 ID
                           --tag 1,2               # 可选:按标签 ID
                           --sort-by created_on    # 可选:排序字段
                           --sort-dir asc          # 可选:排序方向
API 调用
GET /api/v1/{owner}/{repo}/issues?keyword=xxx&category=opened&assigner_id=42&sort_by=issues.updated_on
This commit is contained in:
Surponess 2026-05-25 22:07:35 +08:00
parent 4176555d37
commit 355fa7fdfb
4 changed files with 247 additions and 2 deletions

View File

@ -1,6 +1,7 @@
package search
import (
"fmt"
"net/url"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
@ -50,5 +51,58 @@ func Shortcuts() []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "issues",
Description: "Search issues in a repository",
Flags: []common.Flag{
{Name: "keyword", Short: "k", Usage: "Search keyword", Required: true},
{Name: "category", Short: "c", Usage: "Issue category: all, opened, closed", Default: "all"},
{Name: "assignee", Short: "a", Usage: "Filter by assignee user ID"},
{Name: "author", Usage: "Filter by author user ID"},
{Name: "milestone", Short: "m", Usage: "Filter by milestone ID"},
{Name: "tag", Short: "t", Usage: "Filter by tag IDs (comma-separated)"},
{Name: "sort-by", Usage: "Sort field: updated_on, created_on, priority", Default: "updated_on"},
{Name: "sort-dir", Usage: "Sort direction: asc, desc", Default: "desc"},
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
keyword, _ := ctx.RequireArg("keyword")
q := url.Values{}
q.Set("keyword", keyword)
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
if c := ctx.Arg("category"); c != "" {
q.Set("category", c)
}
if a := ctx.Arg("assignee"); a != "" {
q.Set("assigner_id", a)
}
if a := ctx.Arg("author"); a != "" {
q.Set("author_id", a)
}
if m := ctx.Arg("milestone"); m != "" {
q.Set("milestone_id", m)
}
if t := ctx.Arg("tag"); t != "" {
q.Set("issue_tag_ids", t)
}
if s := ctx.Arg("sort-by"); s != "" {
q.Set("sort_by", "issues."+s)
}
if d := ctx.Arg("sort-dir"); d != "" {
q.Set("sort_direction", d)
}
env, err := ctx.CallAPIWithQuery("GET",
fmt.Sprintf("/v1/%s/%s/issues", ctx.Owner, ctx.Repo), q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}

View File

@ -0,0 +1,126 @@
package search
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestSearchIssuesWithKeyword(t *testing.T) {
var requestQuery string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestQuery = r.URL.RawQuery
common.WriteJSON(t, w, map[string]interface{}{
"total_count": float64(2),
"opened_count": float64(1),
"closed_count": float64(1),
"issues": []interface{}{
map[string]interface{}{
"id": float64(1),
"subject": "Fix login bug",
"project_issues_index": float64(10),
"status_name": "新增",
},
map[string]interface{}{
"id": float64(2),
"subject": "Update login page",
"project_issues_index": float64(11),
"status_name": "关闭",
},
},
})
}))
defer server.Close()
ctx := common.NewTestContext(t, server, "owner", "repo", map[string]string{
"keyword": "login",
})
err := common.RunShortcut(t, Shortcuts(), "issues", ctx)
if err != nil {
t.Fatalf("search issues failed: %v", err)
}
if !strings.Contains(requestQuery, "keyword=login") {
t.Errorf("expected keyword param, got: %s", requestQuery)
}
if !strings.Contains(requestQuery, "category=all") {
t.Errorf("expected category=all default, got: %s", requestQuery)
}
}
func TestSearchIssuesWithAllFilters(t *testing.T) {
var requestQuery string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestQuery = r.URL.RawQuery
common.WriteJSON(t, w, map[string]interface{}{
"total_count": float64(1),
"opened_count": float64(1),
"closed_count": float64(0),
"issues": []interface{}{},
})
}))
defer server.Close()
ctx := common.NewTestContext(t, server, "owner", "repo", map[string]string{
"keyword": "bug",
"category": "opened",
"assignee": "42",
"author": "10",
"milestone": "5",
"tag": "1,2",
"sort-by": "created_on",
"sort-dir": "asc",
})
err := common.RunShortcut(t, Shortcuts(), "issues", ctx)
if err != nil {
t.Fatalf("search issues with filters failed: %v", err)
}
checks := []string{
"keyword=bug",
"category=opened",
"assigner_id=42",
"author_id=10",
"milestone_id=5",
"issue_tag_ids=1%2C2",
"sort_by=issues.created_on",
"sort_direction=asc",
}
for _, want := range checks {
if !strings.Contains(requestQuery, want) {
t.Errorf("missing query param %q in: %s", want, requestQuery)
}
}
}
func TestSearchIssuesRequiresKeyword(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without keyword")
}))
defer server.Close()
ctx := common.NewTestContext(t, server, "owner", "repo", map[string]string{})
err := common.RunShortcut(t, Shortcuts(), "issues", ctx)
if err == nil {
t.Fatal("expected error when keyword is missing, got nil")
}
}
func TestSearchIssuesRequiresOwnerRepo(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without owner/repo")
}))
defer server.Close()
// Empty owner/repo to trigger ResolveOwnerRepo failure
ctx := common.NewTestContext(t, server, "", "repo", map[string]string{
"keyword": "test",
})
err := common.RunShortcut(t, Shortcuts(), "issues", ctx)
if err == nil {
t.Fatal("expected error when owner/repo missing, got nil")
}
}

View File

@ -1,7 +1,7 @@
---
name: gitlink-search
version: 1.0.0
description: "搜索:搜索仓库和用户。当用户需要在 GitLink 上搜索资源时触发。"
version: 1.1.0
description: "搜索:搜索仓库、用户和 Issue。当用户需要在 GitLink 上搜索资源时触发。"
metadata:
requires:
bins: ["gitlink-cli"]
@ -22,6 +22,7 @@ metadata:
|----------|------|
| `search +repos` | 搜索仓库 |
| `search +users` | 搜索用户 |
| `search +issues` | 搜索 Issue需要 owner/repo |
## 使用示例
@ -31,4 +32,16 @@ gitlink-cli search +repos --keyword "machine learning" --limit 10
# 搜索用户
gitlink-cli search +users --keyword "zhangsan"
# 搜索 Issue基本用法
gitlink-cli search +issues --owner MyOrg --repo my-project --keyword "登录失败"
# 搜索已关闭的 Issue
gitlink-cli search +issues -k "bug" --category closed
# 搜索指定负责人和标签的 Issue
gitlink-cli search +issues -k "性能" --assignee 42 --tag 1,2
# 按创建时间正序排列
gitlink-cli search +issues -k "需求" --sort-by created_on --sort-dir asc
```

View File

@ -0,0 +1,52 @@
# search +issues
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
搜索指定仓库中的 Issue疑修。支持关键词搜索、状态筛选、负责人/作者/里程碑/标签过滤、排序等。
## 命令
```bash
# 基本搜索
gitlink-cli search +issues --owner MyOrg --repo my-project --keyword "登录失败"
# 简写
gitlink-cli search +issues -k "bug" --owner MyOrg --repo my-project
# JSON 格式输出
gitlink-cli search +issues -k "bug" --format json
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--keyword` / `-k` | 是 | 搜索关键词 |
| `--category` / `-c` | 否 | Issue 类型:`all`(全部)、`opened`(开启中)、`closed`(已关闭),默认 `all` |
| `--assignee` / `-a` | 否 | 按负责人用户 ID 筛选 |
| `--author` | 否 | 按创建人用户 ID 筛选 |
| `--milestone` / `-m` | 否 | 按里程碑 ID 筛选 |
| `--tag` / `-t` | 否 | 按标签 ID 筛选(多个 ID 用逗号分隔) |
| `--sort-by` | 否 | 排序字段:`updated_on`(默认)、`created_on`、`priority` |
| `--sort-dir` | 否 | 排序方向:`desc`(默认,倒序)、`asc`(正序) |
| `--page` / `-p` | 否 | 页码,默认 1 |
| `--limit` / `-l` | 否 | 每页数量,默认 20 |
## API
```
GET /api/v1/{owner}/{repo}/issues?keyword=xxx&category=opened&page=1&limit=20
```
## 注意事项
- 搜索 Issue 需要指定 `--owner``--repo`(或在一个已配置 git remote 的仓库目录中运行)
- `--tag` 参数接受逗号分隔的标签 ID`--tag 1,2,3`),而非标签名称
- `--assignee``--author` 接受用户 ID数字不是用户名
- 返回数据包含 `total_count`、`opened_count`、`closed_count` 和 `issues` 数组
- 每个 Issue 包含 `project_issues_index`(网页 URL 中的序号)、`subject`、`status_name`、`author`、`assigners` 等字段
## References
- [gitlink-shared SKILL.md](../../gitlink-shared/SKILL.md) -- 认证与全局参数
- [gitlink-search SKILL.md](../SKILL.md) -- 搜索操作总览