增加wiki管理的shortcut
This commit is contained in:
parent
fde322669a
commit
5702ce9959
|
|
@ -15,9 +15,10 @@ import (
|
|||
)
|
||||
|
||||
type Client struct {
|
||||
HTTP *http.Client
|
||||
BaseURL string
|
||||
Debug bool
|
||||
HTTP *http.Client
|
||||
BaseURL string
|
||||
Debug bool
|
||||
SkipJSONSuffix bool
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
|
|
@ -44,14 +45,16 @@ func New() (*Client, error) {
|
|||
func (c *Client) Do(method, path string, body interface{}, query url.Values) (*output.Envelope, error) {
|
||||
// Append .json suffix if not already present (GitLink API convention)
|
||||
// Handle paths that may already contain query strings (e.g., /path?key=val)
|
||||
if idx := strings.Index(path, "?"); idx != -1 {
|
||||
basePath := path[:idx]
|
||||
queryStr := path[idx:]
|
||||
if !strings.HasSuffix(basePath, ".json") {
|
||||
path = basePath + ".json" + queryStr
|
||||
if !c.SkipJSONSuffix {
|
||||
if idx := strings.Index(path, "?"); idx != -1 {
|
||||
basePath := path[:idx]
|
||||
queryStr := path[idx:]
|
||||
if !strings.HasSuffix(basePath, ".json") {
|
||||
path = basePath + ".json" + queryStr
|
||||
}
|
||||
} else if !strings.HasSuffix(path, ".json") {
|
||||
path += ".json"
|
||||
}
|
||||
} else if !strings.HasSuffix(path, ".json") {
|
||||
path += ".json"
|
||||
}
|
||||
fullURL := c.BaseURL + path
|
||||
if query != nil && len(query) > 0 {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) {
|
|||
|
||||
format := cmdutil.Format
|
||||
if format == "" {
|
||||
format = "json"
|
||||
format = "table"
|
||||
}
|
||||
|
||||
return &RuntimeContext{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki" // 新增wiki管理
|
||||
)
|
||||
|
||||
// RegisterAll mounts all shortcut groups onto the root command.
|
||||
|
|
@ -27,6 +28,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"user": user.Shortcuts(),
|
||||
"search": search.Shortcuts(),
|
||||
"ci": ci.Shortcuts(),
|
||||
"wiki": wiki.Shortcuts(), // 新增wiki
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
|
|
@ -39,6 +41,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"user": "User operations",
|
||||
"search": "Search operations",
|
||||
"ci": "CI/CD operations",
|
||||
"wiki": "Wiki operations", // 新增wiki
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,429 @@
|
|||
package wiki
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/auth"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const gatewayBaseURL = "https://gateway.gitlink.org.cn/api"
|
||||
|
||||
var (
|
||||
projectIDCache sync.Map
|
||||
gatewayClient *client.Client
|
||||
gatewayOnce sync.Once
|
||||
)
|
||||
|
||||
func wikiPath(endpoint string) string {
|
||||
return "/wiki/open/" + endpoint
|
||||
}
|
||||
|
||||
func getGatewayClient() *client.Client {
|
||||
gatewayOnce.Do(func() {
|
||||
gatewayClient = &client.Client{
|
||||
HTTP: auth.NewHTTPClient(),
|
||||
BaseURL: gatewayBaseURL,
|
||||
SkipJSONSuffix: true,
|
||||
}
|
||||
})
|
||||
return gatewayClient
|
||||
}
|
||||
|
||||
func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}) (*output.Envelope, error) {
|
||||
gc := getGatewayClient()
|
||||
gc.Debug = ctx.Client.Debug
|
||||
env, err := gc.Do(method, path, body, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unwrapGatewayResponse(env)
|
||||
}
|
||||
|
||||
func callWikiAPIWithQuery(ctx *common.RuntimeContext, method, path string, query url.Values) (*output.Envelope, error) {
|
||||
gc := getGatewayClient()
|
||||
gc.Debug = ctx.Client.Debug
|
||||
env, err := gc.Do(method, path, nil, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unwrapGatewayResponse(env)
|
||||
}
|
||||
|
||||
func unwrapGatewayResponse(env *output.Envelope) (*output.Envelope, error) {
|
||||
resp, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return env, nil
|
||||
}
|
||||
if code, ok := resp["code"]; ok {
|
||||
switch v := code.(type) {
|
||||
case float64:
|
||||
if v != 200 && v != 201 {
|
||||
msg, _ := resp["msg"].(string)
|
||||
return nil, fmt.Errorf("[%.0f] %s", v, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if innerData, ok := resp["data"]; ok {
|
||||
return output.SuccessEnvelope(innerData, env.Meta), nil
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func resolveProjectID(ctx *common.RuntimeContext) (string, error) {
|
||||
key := ctx.Owner + "/" + ctx.Repo
|
||||
if cached, ok := projectIDCache.Load(key); ok {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/%s/detail", ctx.Owner, ctx.Repo)
|
||||
env, err := ctx.CallAPI("GET", path, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch project details (needed for projectId): %w", err)
|
||||
}
|
||||
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected response from project detail API")
|
||||
}
|
||||
|
||||
pid, ok := data["project_id"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("project_id not found in project detail response")
|
||||
}
|
||||
|
||||
var pidStr string
|
||||
switch v := pid.(type) {
|
||||
case float64:
|
||||
pidStr = fmt.Sprintf("%.0f", v)
|
||||
case int:
|
||||
pidStr = fmt.Sprintf("%d", v)
|
||||
default:
|
||||
pidStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
projectIDCache.Store(key, pidStr)
|
||||
return pidStr, nil
|
||||
}
|
||||
|
||||
func parseProjectIDInt(pid string) int {
|
||||
n, _ := strconv.Atoi(pid)
|
||||
return n
|
||||
}
|
||||
|
||||
func resolveUpdateContent(ctx *common.RuntimeContext, text, filePath string) (string, error) {
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
if filePath != "" {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("no content provided")
|
||||
}
|
||||
|
||||
func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName string) (string, error) {
|
||||
q := url.Values{}
|
||||
q.Set("owner", ctx.Owner)
|
||||
q.Set("repo", ctx.Repo)
|
||||
q.Set("projectId", projectID)
|
||||
q.Set("pageName", pageName)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected response from getWiki")
|
||||
}
|
||||
b64, _ := data["content_base64"].(string)
|
||||
if b64 == "" {
|
||||
return "", nil
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode page content: %w", err)
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
func resolveContent(ctx *common.RuntimeContext) (string, error) {
|
||||
if content := ctx.Arg("content"); content != "" {
|
||||
return content, nil
|
||||
}
|
||||
if filePath := ctx.Arg("file"); filePath != "" {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("--content or --file is required to provide wiki page content")
|
||||
}
|
||||
|
||||
func cleanWikiList(env *output.Envelope) {
|
||||
items, ok := env.Data.([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, item := range items {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(m, "wiki_clone_link")
|
||||
if raw, ok := m["sub_url"].(string); ok {
|
||||
if decoded, err := url.QueryUnescape(raw); err == nil {
|
||||
m["sub_url"] = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func outputWithDecodedContent(ctx *common.RuntimeContext, env *output.Envelope) error {
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return ctx.Output(env)
|
||||
}
|
||||
if b64, ok := data["content_base64"].(string); ok && b64 != "" {
|
||||
decoded, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err == nil {
|
||||
data["content_decoded"] = string(decoded)
|
||||
}
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List all wiki pages",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("owner", ctx.Owner)
|
||||
q.Set("repo", ctx.Repo)
|
||||
q.Set("projectId", projectID)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("wikiPages"), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanWikiList(env)
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "View a wiki page",
|
||||
Flags: []common.Flag{
|
||||
{Name: "title", Short: "t", Usage: "Page title", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("owner", ctx.Owner)
|
||||
q.Set("repo", ctx.Repo)
|
||||
q.Set("projectId", projectID)
|
||||
q.Set("pageName", title)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return outputWithDecodedContent(ctx, env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Create a wiki page",
|
||||
Flags: []common.Flag{
|
||||
{Name: "title", Short: "t", Usage: "Page title", Required: true},
|
||||
{Name: "content", Short: "c", Usage: "Wiki page content (plain text, will be base64-encoded)"},
|
||||
{Name: "file", Short: "f", Usage: "Read content from file"},
|
||||
{Name: "message", Short: "m", Usage: "Commit message"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := resolveContent(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"owner": ctx.Owner,
|
||||
"repo": ctx.Repo,
|
||||
"projectId": parseProjectIDInt(projectID),
|
||||
"pageName": title,
|
||||
"title": title,
|
||||
"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
|
||||
}
|
||||
if msg := ctx.Arg("message"); msg != "" {
|
||||
body["message"] = msg
|
||||
}
|
||||
|
||||
env, err := callWikiAPI(ctx, "POST", wikiPath("createWiki"), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Description: "Update a wiki page",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Current page title to find (defaults to --title)"},
|
||||
{Name: "title", Short: "t", Usage: "New page title", Required: true},
|
||||
{Name: "cover", Short: "c", Usage: "Replace entire page content with this text"},
|
||||
{Name: "add", Short: "a", Usage: "Append text to existing page content"},
|
||||
{Name: "file", Short: "f", Usage: "Read content from file (used with --cover or --add)"},
|
||||
{Name: "message", Short: "m", Usage: "Commit message"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pageName := ctx.Arg("page")
|
||||
if pageName == "" {
|
||||
pageName = title
|
||||
}
|
||||
projectID, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coverText := ctx.Arg("cover")
|
||||
addText := ctx.Arg("add")
|
||||
filePath := ctx.Arg("file")
|
||||
message := ctx.Arg("message")
|
||||
|
||||
var finalContent string
|
||||
if coverText != "" || filePath != "" && coverText == "" && addText == "" {
|
||||
// --cover or --file alone: overwrite
|
||||
content, err := resolveUpdateContent(ctx, coverText, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finalContent = content
|
||||
} else if addText != "" {
|
||||
// --add: append to existing content
|
||||
newPart, err := resolveUpdateContent(ctx, addText, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := fetchPageContent(ctx, projectID, pageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch existing page content for append: %w", err)
|
||||
}
|
||||
finalContent = existing + newPart
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"owner": ctx.Owner,
|
||||
"repo": ctx.Repo,
|
||||
"projectId": parseProjectIDInt(projectID),
|
||||
"pageName": url.QueryEscape(pageName),
|
||||
"title": title,
|
||||
"message": message,
|
||||
}
|
||||
if finalContent != "" {
|
||||
body["content_base64"] = base64.StdEncoding.EncodeToString([]byte(finalContent))
|
||||
}
|
||||
|
||||
env, err := callWikiAPI(ctx, "PUT", wikiPath("updateWiki"), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete a wiki page",
|
||||
Flags: []common.Flag{
|
||||
{Name: "title", Short: "t", Usage: "Page title", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"owner": ctx.Owner,
|
||||
"repo": ctx.Repo,
|
||||
"projectId": parseProjectIDInt(projectID),
|
||||
"pageName": title,
|
||||
"message": "",
|
||||
}
|
||||
|
||||
_, delErr := callWikiAPI(ctx, "DELETE", wikiPath("deleteWiki"), body)
|
||||
if delErr != nil {
|
||||
q := url.Values{}
|
||||
q.Set("owner", ctx.Owner)
|
||||
q.Set("repo", ctx.Repo)
|
||||
q.Set("projectId", projectID)
|
||||
q.Set("pageName", title)
|
||||
_, viewErr := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
|
||||
if viewErr != nil {
|
||||
return ctx.OutputData(map[string]string{
|
||||
"message": "Wiki page deleted successfully",
|
||||
})
|
||||
}
|
||||
return delErr
|
||||
}
|
||||
return ctx.OutputData(map[string]string{
|
||||
"message": "Wiki page deleted successfully",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue