feat(list): 新增 tag +list 命令组、watchers/stargazers 分页;修复翻页助手服务端封顶 limit 丢数据 bug
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
8958a686a6
commit
1ca60f9280
|
|
@ -16,13 +16,14 @@
|
|||
- 遵循 `total_count`:达到总数即停止;另设最大页数护栏,防止
|
||||
忽略 `page` 参数的端点造成死循环。
|
||||
- `PaginateAll` 保持原签名,委托给 `PaginateAllKey`。
|
||||
- 十二个分页 list 命令新增 `--all` 布尔参数(默认 false):
|
||||
- 十五个分页 list 命令新增 `--all` 布尔参数(默认 false):
|
||||
- `issue +list --all`(合并结果同样应用 number/database_id 规范化)
|
||||
- `pr +list --all`、`branch +list --all`、`release +list --all`
|
||||
- `milestone +list --all`、`org +list --all`、`repo +list --all`
|
||||
- `search +repos --all`、`search +users --all`
|
||||
- `label +list --all`、`member +list --all`、`webhook +list --all`
|
||||
- `issue +comments --all`(新增子命令,见下)
|
||||
- `tag +list --all`(新增命令组,见下)、`repo +watchers/+stargazers --all`
|
||||
- 对应资源键:`issues`/`pulls`/`branches`/`releases`/`milestones`/
|
||||
`organizations`/`projects`/`users`/`issue_tags`/`collaborators`/`webhooks`
|
||||
(均生产实测确认)
|
||||
|
|
@ -33,7 +34,16 @@
|
|||
- `member +list` 既无分页又走遗留路径(非管理员直接 403),现改走
|
||||
`/v1/:owner/:repo/collaborators`(支持分页且普通成员可读);
|
||||
- `webhook +list` 完全没有 `--page/--limit`(探针实测:建 3 个 webhook 后
|
||||
`page=2&limit=1` 返回第二条,确认端点分页),现已补齐。
|
||||
`page=2&limit=1` 返回第二条,确认端点分页),现已补齐;
|
||||
- `repo +watchers/+stargazers` 完全没有分页 flag(端点实测分页,
|
||||
总数键 `count`,forgeplus watchers 264 / stargazers 577),现已补齐。
|
||||
- 新增 `tag +list` 命令组:平台暴露分页的 `/v1/:owner/:repo/tags`
|
||||
端点(轻量 tag 与 release 不同),但 CLI 此前完全没有 tag 命令;
|
||||
生产实测 forgeplus 16 个 tag 分页与 --all 合并均通过。
|
||||
- 修复翻页助手服务端封顶 limit 丢数据 bug:当端点把请求的 limit
|
||||
封顶(如请求 100 每页只返 20)时,旧逻辑因「页内条数 < limit」提前
|
||||
终止只拿到首页;现已知 total 时以 total 为准(watchers 264 条全量
|
||||
合并生产实测),新增回归单测。
|
||||
- 新增 `issue +comments` 子命令(对标 `gh issue view --comments`):
|
||||
此前 CLI 只能发评论(`issue +comment`)无法读评论流,Agent 无法获取
|
||||
issue 讨论上下文;现接 `/v1/:owner/:repo/issues/:number/journals`
|
||||
|
|
|
|||
|
|
@ -64,8 +64,11 @@ func (c *Client) PaginateAllKey(path string, params url.Values, listKey string)
|
|||
if totalCount >= 0 && len(all) >= totalCount {
|
||||
break
|
||||
}
|
||||
// A short page only signals the end when the endpoint does not
|
||||
// report a total: servers may cap the requested limit (e.g. ask
|
||||
// for 100, get 20 per page), so with a known total we rely on it.
|
||||
limit, _ := strconv.Atoi(params.Get("limit"))
|
||||
if len(items) < limit {
|
||||
if totalCount < 0 && len(items) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -152,3 +153,30 @@ func TestPaginateAllKeyMissingKeyNotList(t *testing.T) {
|
|||
t.Fatalf("len(items) = %d, want 1 (single-object fallback)", len(items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginateAllKeyServerCappedLimit(t *testing.T) {
|
||||
// The server caps every page at 2 items regardless of the requested
|
||||
// limit; with total_count reported, all 5 items must still be fetched.
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
start := (page - 1) * 2
|
||||
var items []string
|
||||
for i := start; i < start+2 && i < 5; i++ {
|
||||
items = append(items, fmt.Sprintf(`{"id":%d}`, i))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"count":5,"users":[%s]}`, strings.Join(items, ","))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
|
||||
params := url.Values{}
|
||||
params.Set("limit", "100")
|
||||
items, err := c.PaginateAllKey("/thing", params, "users")
|
||||
if err != nil {
|
||||
t.Fatalf("PaginateAllKey: %v", err)
|
||||
}
|
||||
if len(items) != 5 {
|
||||
t.Fatalf("len(items) = %d, want 5", len(items))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/tag"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
|
||||
|
|
@ -50,6 +51,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"tag": tag.Shortcuts(),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"dataset": dataset.Shortcuts(tr),
|
||||
|
|
@ -75,6 +77,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"tag": "Git tag operations",
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"dataset": tr.T("cmd.dataset.short"),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
|
||||
expectedGroups := []string{
|
||||
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"org", "user", "search", "tag", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"dataset", "health", "ignore", "wiki",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,6 +334,15 @@ func runCommunityList(ctx *common.RuntimeContext, path string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if ctx.Arg("all") == "true" {
|
||||
items, err := ctx.PaginateAllKey(ctx.RepoPath()+"/"+path, q, "users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(common.NewListEnvelope("users", items))
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/"+path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -345,6 +354,9 @@ func communityListFlags() []common.Flag {
|
|||
return []common.Flag{
|
||||
{Name: "start-at", Usage: "Start timestamp"},
|
||||
{Name: "end-at", Usage: "End timestamp"},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
{Name: "all", Usage: "Fetch all pages automatically (ignores --page)", Bool: true, Default: "false"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// Shortcuts returns git tag shortcuts.
|
||||
//
|
||||
// Tags previously had no first-class command even though the platform
|
||||
// exposes a paginated v1 endpoint; releases only cover annotated releases,
|
||||
// while lightweight tags were reachable through the raw API alone.
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List repository git tags",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
{Name: "all", Usage: "Fetch all pages automatically (ignores --page)", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/v1/%s/%s/tags", ctx.Owner, ctx.Repo)
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if ctx.Arg("all") == "true" {
|
||||
items, err := ctx.PaginateAllKey(path, q, "tags")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(common.NewListEnvelope("tags", items))
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue