Merge pull request 'feat(wiki): --project-id 可选化,省略时自动从仓库详情解析(免手工查询平台内部 ID)' (#372) from Taoyouce/gitlink-cli:feat/wiki-project-id-autoresolve into master

This commit is contained in:
wbtiger 2026-07-14 20:44:21 +08:00
commit 35743c8ae6
4 changed files with 182 additions and 31 deletions

View File

@ -278,10 +278,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

View File

@ -289,10 +289,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 页面

View File

@ -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)

View File

@ -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)