diff --git a/shortcuts/search/search.go b/shortcuts/search/search.go index 443dec3..23b819b 100644 --- a/shortcuts/search/search.go +++ b/shortcuts/search/search.go @@ -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) + }, + }, } } diff --git a/shortcuts/search/search_test.go b/shortcuts/search/search_test.go new file mode 100644 index 0000000..86e1f77 --- /dev/null +++ b/shortcuts/search/search_test.go @@ -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") + } +} diff --git a/skills/gitlink-search/SKILL.md b/skills/gitlink-search/SKILL.md index da2397b..3b94eee 100644 --- a/skills/gitlink-search/SKILL.md +++ b/skills/gitlink-search/SKILL.md @@ -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 ``` diff --git a/skills/gitlink-search/references/gitlink-search-issues.md b/skills/gitlink-search/references/gitlink-search-issues.md new file mode 100644 index 0000000..d1b63e7 --- /dev/null +++ b/skills/gitlink-search/references/gitlink-search-issues.md @@ -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) -- 搜索操作总览