From 794210ec039118ccfc340fc5c540de09cb14f108 Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Wed, 8 Jul 2026 12:27:26 +0000 Subject: [PATCH] feat(wiki): auto-resolve --project-id from repository detail when omitted --- README.md | 7 ++- README.zh-CN.md | 7 ++- shortcuts/wiki/wiki.go | 115 +++++++++++++++++++++++++++--------- shortcuts/wiki/wiki_test.go | 84 ++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index e5e4318..c4efb50 100644 --- a/README.md +++ b/README.md @@ -277,10 +277,13 @@ gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ### Wiki Management ```bash -# List wiki pages (table of contents) -gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 +# List wiki pages (table of contents); --project-id is auto-resolved from the repository when omitted +gitlink-cli wiki +list --owner Gitlink --repo forgeplus # View a wiki page by page name +gitlink-cli wiki +view --owner Gitlink --repo forgeplus -n home + +# Pass --project-id explicitly to skip the extra lookup request gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home # Create a wiki page diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..d0dc4b3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -288,10 +288,13 @@ gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ### Wiki 管理 ```bash -# 列出 Wiki 页面(目录结构) -gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 +# 列出 Wiki 页面(目录结构);省略 --project-id 时自动从仓库信息解析 +gitlink-cli wiki +list --owner Gitlink --repo forgeplus # 查看 Wiki 页面 +gitlink-cli wiki +view --owner Gitlink --repo forgeplus -n home + +# 也可显式传 --project-id 以省去一次查询请求 gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home # 创建 Wiki 页面 diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index 539b7a1..70e19fe 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -4,6 +4,8 @@ import ( "encoding/base64" "fmt" "net/url" + "strconv" + "strings" "github.com/gitlink-org/gitlink-cli/internal/config" "github.com/gitlink-org/gitlink-cli/shortcuts/common" @@ -27,6 +29,45 @@ func gatewayFlag() common.Flag { return common.Flag{Name: "gateway", Short: "g", Usage: "Use gateway API endpoint", Bool: true} } +// projectIDFlag returns the common --project-id flag definition. +func projectIDFlag() common.Flag { + return common.Flag{Name: "project-id", Usage: "GitLink project ID (auto-resolved from the repository when omitted)"} +} + +// resolveProjectID returns the explicit --project-id value, or resolves it +// from the repository detail endpoint on the main API. It must be called +// before switching the client to the gateway base URL. +func resolveProjectID(ctx *common.RuntimeContext) (string, error) { + if raw := strings.TrimSpace(ctx.Arg("project-id")); raw != "" { + parsed, err := strconv.ParseInt(raw, 10, 64) + if err != nil || parsed <= 0 { + return "", fmt.Errorf("invalid --project-id %q: use a positive numeric project ID", raw) + } + return strconv.FormatInt(parsed, 10), nil + } + env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil) + if err != nil { + return "", fmt.Errorf("resolve project id: %w", err) + } + data, ok := env.Data.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("resolve project id: unexpected repository response") + } + for _, key := range []string{"project_id", "id"} { + switch v := data[key].(type) { + case float64: + if v > 0 { + return strconv.FormatInt(int64(v), 10), nil + } + case string: + if s := strings.TrimSpace(v); s != "" { + return s, nil + } + } + } + return "", fmt.Errorf("resolve project id: repository response did not include project_id; pass --project-id explicitly") +} + // Shortcuts returns all wiki shortcuts. func Shortcuts() []*common.Shortcut { return []*common.Shortcut{ @@ -34,22 +75,26 @@ func Shortcuts() []*common.Shortcut { Name: "list", Description: "List wiki pages", Flags: []common.Flag{ - {Name: "project-id", Usage: "GitLink project ID", Required: true}, + projectIDFlag(), gatewayFlag(), }, Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } if ctx.Arg("gateway") == "true" { if err := switchToGateway(ctx); err != nil { return err } } - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } q := url.Values{} q.Set("owner", ctx.Owner) q.Set("repo", ctx.Repo) - q.Set("projectId", ctx.Arg("project-id")) + q.Set("projectId", projectID) env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/wikiPages", q) if err != nil { return err @@ -61,23 +106,27 @@ func Shortcuts() []*common.Shortcut { Name: "view", Description: "View a wiki page by page name", Flags: []common.Flag{ - {Name: "project-id", Usage: "GitLink project ID", Required: true}, + projectIDFlag(), {Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true}, gatewayFlag(), }, Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } if ctx.Arg("gateway") == "true" { if err := switchToGateway(ctx); err != nil { return err } } - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } q := url.Values{} q.Set("owner", ctx.Owner) q.Set("repo", ctx.Repo) - q.Set("projectId", ctx.Arg("project-id")) + q.Set("projectId", projectID) q.Set("pageName", ctx.Arg("page-name")) env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/getWiki", q) if err != nil { @@ -90,7 +139,7 @@ func Shortcuts() []*common.Shortcut { Name: "create", Description: "Create a new wiki page", Flags: []common.Flag{ - {Name: "project-id", Usage: "GitLink project ID", Required: true}, + projectIDFlag(), {Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true}, {Name: "title", Short: "t", Usage: "Wiki page title", Required: true}, {Name: "content", Short: "c", Usage: "Wiki page content (markdown)", Required: true}, @@ -98,19 +147,23 @@ func Shortcuts() []*common.Shortcut { gatewayFlag(), }, Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } if ctx.Arg("gateway") == "true" { if err := switchToGateway(ctx); err != nil { return err } } - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } content := ctx.Arg("content") payload := map[string]interface{}{ "owner": ctx.Owner, "repo": ctx.Repo, - "projectId": ctx.Arg("project-id"), + "projectId": projectID, "pageName": ctx.Arg("page-name"), "title": ctx.Arg("title"), "content_base64": base64.StdEncoding.EncodeToString([]byte(content)), @@ -127,7 +180,7 @@ func Shortcuts() []*common.Shortcut { Name: "update", Description: "Update an existing wiki page", Flags: []common.Flag{ - {Name: "project-id", Usage: "GitLink project ID", Required: true}, + projectIDFlag(), {Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true}, {Name: "title", Short: "t", Usage: "Wiki page title", Required: true}, {Name: "content", Short: "c", Usage: "Wiki page content (markdown)"}, @@ -135,11 +188,6 @@ func Shortcuts() []*common.Shortcut { gatewayFlag(), }, Run: func(ctx *common.RuntimeContext) error { - if ctx.Arg("gateway") == "true" { - if err := switchToGateway(ctx); err != nil { - return err - } - } if err := ctx.ResolveOwnerRepo(); err != nil { return err } @@ -147,11 +195,20 @@ func Shortcuts() []*common.Shortcut { if title == "" { return fmt.Errorf("--title is required") } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } + } content := ctx.Arg("content") payload := map[string]interface{}{ "owner": ctx.Owner, "repo": ctx.Repo, - "projectId": ctx.Arg("project-id"), + "projectId": projectID, "pageName": ctx.Arg("page-name"), "title": title, "message": ctx.Arg("message"), @@ -170,23 +227,27 @@ func Shortcuts() []*common.Shortcut { Name: "delete", Description: "Delete a wiki page", Flags: []common.Flag{ - {Name: "project-id", Usage: "GitLink project ID", Required: true}, + projectIDFlag(), {Name: "page-name", Short: "n", Usage: "Wiki page name (slug)", Required: true}, gatewayFlag(), }, Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } if ctx.Arg("gateway") == "true" { if err := switchToGateway(ctx); err != nil { return err } } - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } payload := map[string]interface{}{ "owner": ctx.Owner, "repo": ctx.Repo, - "projectId": ctx.Arg("project-id"), + "projectId": projectID, "pageName": ctx.Arg("page-name"), } env, err := ctx.CallAPI("DELETE", "/wiki/open/deleteWiki", payload) diff --git a/shortcuts/wiki/wiki_test.go b/shortcuts/wiki/wiki_test.go index f0d95d5..7d9cd35 100644 --- a/shortcuts/wiki/wiki_test.go +++ b/shortcuts/wiki/wiki_test.go @@ -162,6 +162,90 @@ func TestWikiDelete(t *testing.T) { assertEqual(t, payload["pageName"], "old-page") } +func TestWikiListAutoResolvesProjectID(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + switch requests { + case 1: + assertRequest(t, r, "GET", "/owner/repo.json") + writeJSON(t, w, map[string]interface{}{"project_id": float64(1549132)}) + case 2: + assertRequest(t, r, "GET", "/wiki/open/wikiPages") + assertEqual(t, r.URL.Query().Get("projectId"), "1549132") + writeJSON(t, w, map[string]interface{}{"status": 0, "data": []interface{}{}}) + default: + t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path) + } + })) + defer server.Close() + + err := runWikiShortcut(t, server, "list", nil) + if err != nil { + t.Fatalf("list with auto-resolved project id failed: %v", err) + } + if requests != 2 { + t.Fatalf("expected 2 requests, got %d", requests) + } +} + +func TestWikiCreateAutoResolvesProjectID(t *testing.T) { + requests := 0 + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + switch requests { + case 1: + assertRequest(t, r, "GET", "/owner/repo.json") + writeJSON(t, w, map[string]interface{}{"project_id": float64(789)}) + case 2: + assertRequest(t, r, "POST", "/wiki/open/createWiki") + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + default: + t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path) + } + })) + defer server.Close() + + err := runWikiShortcut(t, server, "create", map[string]string{ + "page-name": "new-page", + "title": "New Page", + "content": "# Hello", + }) + if err != nil { + t.Fatalf("create with auto-resolved project id failed: %v", err) + } + assertEqual(t, payload["projectId"], "789") +} + +func TestWikiInvalidProjectID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("server should not be called for invalid --project-id: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runWikiShortcut(t, server, "list", map[string]string{ + "project-id": "abc", + }) + if err == nil { + t.Fatal("expected invalid --project-id to return an error") + } +} + +func TestWikiResolveProjectIDMissingField(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/owner/repo.json") + writeJSON(t, w, map[string]interface{}{"identifier": "repo"}) + })) + defer server.Close() + + err := runWikiShortcut(t, server, "list", nil) + if err == nil { + t.Fatal("expected missing project_id in repository response to return an error") + } +} + func runWikiShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { t.Helper() shortcut := findWikiShortcut(t, name)