forked from Gitlink/gitlink-cli
428 lines
13 KiB
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
|
|
}
|