gitlink-cli/shortcuts/file/file.go

428 lines
13 KiB
Go

package file
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// v1RepoPath returns /v1/{owner}/{repo}
func v1RepoPath(ctx *common.RuntimeContext) string {
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
}
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
// === 浏览类 ===
{
Name: "ls",
Description: "List files in root directory",
Flags: []common.Flag{
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/entries", q)
if err != nil {
return fmt.Errorf("获取目录列表失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "tree",
Description: "Show subdirectory or file details",
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "File or directory path", Required: true},
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
filepath, err := ctx.RequireArg("path", "--path src/main.go")
if err != nil {
return err
}
q := url.Values{}
q.Set("filepath", filepath)
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
if err != nil {
return fmt.Errorf("获取路径详情失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "read",
Description: "Read file content",
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "File path", Required: true},
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
filepath, err := ctx.RequireArg("path", "--path README.md")
if err != nil {
return err
}
q := url.Values{}
q.Set("filepath", filepath)
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "readme",
Description: "Read README file",
Flags: []common.Flag{
{Name: "path", Usage: "Subdirectory path for nested README"},
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
if p := ctx.Arg("path"); p != "" {
q.Set("filepath", p)
}
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/readme", q)
if err != nil {
return fmt.Errorf("读取 README 失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "search",
Description: "Search files by name",
Flags: []common.Flag{
{Name: "q", Short: "q", Usage: "Search keyword"},
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
if kw := ctx.Arg("q"); kw != "" {
q.Set("search", kw)
}
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
if err != nil {
return fmt.Errorf("搜索文件失败: %w", err)
}
return ctx.Output(env)
},
},
// === 文件 CRUD ===
{
Name: "create",
Description: "Create a new file",
DryRun: true,
DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
return fmt.Sprintf("创建文件 %s", ctx.Arg("path")), nil
},
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "File path", Required: true},
{Name: "content", Short: "c", Usage: "File content (plain text)", Required: true},
{Name: "branch", Short: "b", Usage: "Target branch", Required: true},
{Name: "message", Short: "m", Usage: "Commit message", Required: true},
{Name: "new-branch", Usage: "Create on a new branch"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
filepath, err := ctx.RequireArg("path", "--path docs/new.md")
if err != nil {
return err
}
content, err := ctx.RequireArg("content", `--content "# Hello"`)
if err != nil {
return err
}
branch, err := ctx.RequireArg("branch", "--branch master")
if err != nil {
return err
}
message, err := ctx.RequireArg("message", `--message "add new file"`)
if err != nil {
return err
}
body := map[string]interface{}{
"filepath": filepath,
"base64_filepath": base64.StdEncoding.EncodeToString([]byte(filepath)),
"branch": branch,
"content": base64.StdEncoding.EncodeToString([]byte(content)),
"message": message,
}
if nb := ctx.Arg("new-branch"); nb != "" {
body["new_branch"] = nb
}
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/create_file", body)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "update",
Description: "Update an existing file",
DryRun: true,
DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
return fmt.Sprintf("更新文件 %s", ctx.Arg("path")), nil
},
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "File path", Required: true},
{Name: "content", Short: "c", Usage: "New file content (plain text)", Required: true},
{Name: "branch", Short: "b", Usage: "Target branch", Required: true},
{Name: "sha", Usage: "File SHA (auto-fetched if omitted)"},
{Name: "message", Short: "m", Usage: "Commit message", Required: true},
{Name: "new-branch", Usage: "Create on a new branch"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
filepath, err := ctx.RequireArg("path", "--path README.md")
if err != nil {
return err
}
content, err := ctx.RequireArg("content", `--content "updated content"`)
if err != nil {
return err
}
branch, err := ctx.RequireArg("branch", "--branch master")
if err != nil {
return err
}
message, err := ctx.RequireArg("message", `--message "update file"`)
if err != nil {
return err
}
// Auto-fetch sha if not provided
sha := ctx.Arg("sha")
if sha == "" {
sha, err = fetchFileSha(ctx, filepath, branch)
if err != nil {
return fmt.Errorf("自动获取文件 SHA 失败,请用 --sha 手动指定: %w", err)
}
}
body := map[string]interface{}{
"filepath": filepath,
"branch": branch,
"content": content,
"sha": sha,
"message": message,
}
if nb := ctx.Arg("new-branch"); nb != "" {
body["new_branch"] = nb
}
env, err := ctx.CallAPI("PUT", ctx.RepoPath()+"/update_file", body)
if err != nil {
return fmt.Errorf("更新文件失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "delete",
Description: "Delete a file",
DryRun: true,
DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
return fmt.Sprintf("删除文件 %s", ctx.Arg("path")), nil
},
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "File path", Required: true},
{Name: "branch", Short: "b", Usage: "Target branch", Required: true},
{Name: "sha", Usage: "File SHA (auto-fetched if omitted)"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
filepath, err := ctx.RequireArg("path", "--path old-file.txt")
if err != nil {
return err
}
branch, err := ctx.RequireArg("branch", "--branch master")
if err != nil {
return err
}
// Auto-fetch sha if not provided
sha := ctx.Arg("sha")
if sha == "" {
sha, err = fetchFileSha(ctx, filepath, branch)
if err != nil {
return fmt.Errorf("自动获取文件 SHA 失败,请用 --sha 手动指定: %w", err)
}
}
body := map[string]interface{}{
"filepath": filepath,
"branch": branch,
"sha": sha,
}
env, err := ctx.CallAPI("DELETE", ctx.RepoPath()+"/delete_file", body)
if err != nil {
return fmt.Errorf("删除文件失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "batch",
Description: "Batch create/update/delete files in one commit",
DryRun: true,
DryRunHint: func(ctx *common.RuntimeContext) (string, error) {
return fmt.Sprintf("批量提交文件到分支 %s", ctx.Arg("branch")), nil
},
Flags: []common.Flag{
{Name: "branch", Short: "b", Usage: "Target branch", Required: true},
{Name: "message", Short: "m", Usage: "Commit message", Required: true},
{Name: "files", Short: "f", Usage: "JSON array: [{\"action_type\":\"create\",\"file_path\":\"x\",\"content\":\"y\"}]", Required: true},
{Name: "new-branch", Usage: "Create on a new branch"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
branch, err := ctx.RequireArg("branch", "--branch master")
if err != nil {
return err
}
message, err := ctx.RequireArg("message", `--message "batch update"`)
if err != nil {
return err
}
filesJSON, err := ctx.RequireArg("files", `--files '[{"action_type":"create","file_path":"a.txt","content":"hello"}]'`)
if err != nil {
return err
}
var files []map[string]interface{}
if err := json.Unmarshal([]byte(filesJSON), &files); err != nil {
return fmt.Errorf("--files JSON 解析失败: %w", err)
}
body := map[string]interface{}{
"branch": branch,
"message": message,
"files": files,
}
if nb := ctx.Arg("new-branch"); nb != "" {
body["new_branch"] = nb
}
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/contents/batch", body)
if err != nil {
return fmt.Errorf("批量提交失败: %w", err)
}
return ctx.Output(env)
},
},
// === Git 对象 ===
{
Name: "commits",
Description: "List commit history",
Flags: []common.Flag{
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/commits", q)
if err != nil {
return fmt.Errorf("获取提交历史失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "diff",
Description: "Show diff of a commit",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha", "--sha abc1234")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/commits/%s/diff", v1RepoPath(ctx), sha), nil)
if err != nil {
return fmt.Errorf("获取 diff 失败: %w", err)
}
return ctx.Output(env)
},
},
}
}
// fetchFileSha retrieves the current SHA of a file via sub_entries API.
func fetchFileSha(ctx *common.RuntimeContext, filepath, ref string) (string, error) {
q := url.Values{}
q.Set("filepath", filepath)
if ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
if err != nil {
return "", err
}
data, ok := env.Data.(map[string]interface{})
if !ok {
return "", fmt.Errorf("响应格式异常")
}
entries, ok := data["entries"].(map[string]interface{})
if !ok {
// Try flat structure
if sha, ok := data["sha"].(string); ok {
return sha, nil
}
return "", fmt.Errorf("响应中未找到 entries 或 sha 字段")
}
sha, _ := entries["sha"].(string)
if sha == "" {
return "", fmt.Errorf("文件 SHA 为空")
}
return sha, nil
}