forked from Gitlink/gitlink-cli
本次实现: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:
parent
4176555d37
commit
355fa7fdfb
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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) -- 搜索操作总览
|
||||
Loading…
Reference in New Issue