feat(wiki): --project-id 可选化,省略时自动从仓库详情解析(免手工查询平台内部 ID) #372
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 页面
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue