|
|
|
|
@ -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",
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|