1011 lines
28 KiB
Go
1011 lines
28 KiB
Go
package repo
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
)
|
|
|
|
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|
tr := shortcutTranslator(translators...)
|
|
return []*common.Shortcut{
|
|
{
|
|
Name: "list",
|
|
Description: tr.T("cmd.repo.list.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "user", Short: "u", Usage: tr.T("flag.user"), Default: ""},
|
|
{Name: "category", Short: "c", Usage: tr.T("flag.repo.category"), Default: "manage"},
|
|
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
|
{Name: "all", Usage: tr.T("flag.all"), Bool: true, Default: "false"},
|
|
},
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
user := ctx.Arg("user")
|
|
q := url.Values{}
|
|
q.Set("page", ctx.Arg("page"))
|
|
q.Set("limit", ctx.Arg("limit"))
|
|
if cat := ctx.Arg("category"); cat != "" && cat != "all" {
|
|
q.Set("category", cat)
|
|
}
|
|
|
|
path := "/projects"
|
|
if user != "" {
|
|
path = fmt.Sprintf("/users/%s/projects", user)
|
|
}
|
|
if ctx.Arg("all") == "true" {
|
|
items, err := ctx.PaginateAllKey(path, q, "projects")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(common.NewListEnvelope("projects", items))
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "info",
|
|
Description: tr.T("cmd.repo.info.short"),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "readme",
|
|
Description: "Show repository README content",
|
|
Flags: []common.Flag{
|
|
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
|
|
{Name: "path", Usage: "README directory path"},
|
|
},
|
|
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)
|
|
}
|
|
if path := ctx.Arg("path"); path != "" {
|
|
q.Set("filepath", path)
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/readme", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "tree",
|
|
Description: tr.T("cmd.repo.tree.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "path", Short: "p", Usage: tr.T("flag.repo.tree.path")},
|
|
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.tree.ref"), Default: "master"},
|
|
},
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
ref := ctx.Arg("ref")
|
|
if ref == "" {
|
|
ref = "master"
|
|
}
|
|
if path := ctx.Arg("path"); path != "" {
|
|
q.Set("filepath", path)
|
|
}
|
|
q.Set("ref", ref)
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "files",
|
|
Description: tr.T("cmd.repo.files.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "search", Short: "s", Usage: tr.T("flag.repo.files.search")},
|
|
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")},
|
|
},
|
|
Run: runFiles,
|
|
},
|
|
{
|
|
Name: "commits",
|
|
Description: tr.T("cmd.repo.commits.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")},
|
|
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
|
},
|
|
Run: runCommits,
|
|
},
|
|
{
|
|
Name: "commit-files",
|
|
Description: tr.T("cmd.repo.commit_files.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true},
|
|
{Name: "file", Short: "f", Usage: tr.T("flag.repo.file")},
|
|
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
|
},
|
|
Run: runCommitFiles,
|
|
},
|
|
{
|
|
Name: "commit-diff",
|
|
Description: tr.T("cmd.repo.commit_diff.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true},
|
|
},
|
|
Run: runCommitDiff,
|
|
},
|
|
{
|
|
Name: "tags",
|
|
Description: tr.T("cmd.repo.tags.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name_filter")},
|
|
{Name: "only-name", Usage: tr.T("flag.repo.only_name"), Default: "false"},
|
|
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
|
},
|
|
Run: runTags,
|
|
},
|
|
{
|
|
Name: "tag",
|
|
Description: tr.T("cmd.repo.tag.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true},
|
|
},
|
|
Run: runTag,
|
|
},
|
|
{
|
|
Name: "delete-tag",
|
|
Description: tr.T("cmd.repo.delete_tag.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true},
|
|
{Name: "dry-run", Usage: tr.T("flag.repo.delete_tag_dry_run"), Bool: true, Default: "false"},
|
|
{Name: "yes", Usage: tr.T("flag.repo.delete_tag_yes"), Bool: true, Default: "false"},
|
|
},
|
|
Run: runDeleteTag,
|
|
},
|
|
{
|
|
Name: "batch-commit",
|
|
Description: tr.T("cmd.repo.batch_commit.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "branch", Short: "b", Usage: tr.T("flag.repo.batch_branch"), Required: true},
|
|
{Name: "message", Short: "m", Usage: tr.T("flag.repo.batch_message"), Required: true},
|
|
{Name: "files", Short: "f", Usage: tr.T("flag.repo.batch_files"), Required: true},
|
|
{Name: "new-branch", Usage: tr.T("flag.repo.batch_new_branch")},
|
|
{Name: "encoding", Usage: tr.T("flag.repo.batch_encoding"), Default: "text"},
|
|
{Name: "author-name", Usage: tr.T("flag.repo.author_name")},
|
|
{Name: "author-email", Usage: tr.T("flag.repo.author_email")},
|
|
{Name: "committer-name", Usage: tr.T("flag.repo.committer_name")},
|
|
{Name: "committer-email", Usage: tr.T("flag.repo.committer_email")},
|
|
{Name: "dry-run", Usage: tr.T("flag.repo.batch_dry_run"), Bool: true, Default: "false"},
|
|
{Name: "yes", Usage: tr.T("flag.repo.batch_yes"), Bool: true, Default: "false"},
|
|
},
|
|
Run: runBatchCommit,
|
|
},
|
|
{
|
|
Name: "languages",
|
|
Description: "Show repository language statistics",
|
|
Run: runLanguages,
|
|
},
|
|
{
|
|
Name: "contributors",
|
|
Description: "List repository contributors",
|
|
Run: runContributors,
|
|
},
|
|
{
|
|
Name: "contributor-stats",
|
|
Description: "List contributor statistics with code line counts",
|
|
Flags: []common.Flag{
|
|
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
|
|
{Name: "pass-year", Usage: "Number of past years to include"},
|
|
},
|
|
Run: runContributorStats,
|
|
},
|
|
{
|
|
Name: "code-stats",
|
|
Description: "Show repository code statistics",
|
|
Flags: []common.Flag{
|
|
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
|
|
},
|
|
Run: runCodeStats,
|
|
},
|
|
{
|
|
Name: "watchers",
|
|
Description: "List repository watchers",
|
|
Flags: communityListFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runCommunityList(ctx, "watchers")
|
|
},
|
|
},
|
|
{
|
|
Name: "stargazers",
|
|
Description: "List repository stargazers",
|
|
Flags: communityListFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runCommunityList(ctx, "stargazers")
|
|
},
|
|
},
|
|
{
|
|
Name: "forks",
|
|
Description: "List repository forks",
|
|
Flags: []common.Flag{
|
|
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
|
{Name: "all", Usage: "Fetch all pages automatically (ignores --page)", Bool: true, Default: "false"},
|
|
},
|
|
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 ctx.Arg("all") == "true" {
|
|
items, err := ctx.PaginateAllKey(ctx.RepoPath()+"/forks", q, "users")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(common.NewListEnvelope("users", items))
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/forks", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "follow",
|
|
Description: "Follow a repository",
|
|
Flags: repoInteractionFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runRepoFollowAction(ctx, "POST", "/watchers/follow", "follow")
|
|
},
|
|
},
|
|
{
|
|
Name: "unfollow",
|
|
Description: "Unfollow a repository",
|
|
Flags: repoInteractionFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runRepoFollowAction(ctx, "DELETE", "/watchers/unfollow", "unfollow")
|
|
},
|
|
},
|
|
{
|
|
Name: "like",
|
|
Description: "Like a repository",
|
|
Flags: repoInteractionFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runRepoPraiseAction(ctx, "POST", "like")
|
|
},
|
|
},
|
|
{
|
|
Name: "unlike",
|
|
Description: "Unlike a repository",
|
|
Flags: repoInteractionFlags(),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return runRepoPraiseAction(ctx, "DELETE", "unlike")
|
|
},
|
|
},
|
|
{
|
|
Name: "create",
|
|
Description: tr.T("cmd.repo.create.short"),
|
|
Flags: []common.Flag{
|
|
{Name: "name", Short: "n", Usage: tr.T("flag.repo.name"), Required: true},
|
|
{Name: "description", Short: "d", Usage: tr.T("flag.repo.description")},
|
|
{Name: "private", Usage: tr.T("flag.repo.private"), Default: "false"},
|
|
},
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
name, err := ctx.RequireArg("name")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Get current user login for the create path
|
|
userEnv, err := ctx.CallAPI("GET", "/users/me", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("获取当前用户信息失败: %w", err)
|
|
}
|
|
userData, _ := userEnv.Data.(map[string]interface{})
|
|
login, _ := userData["login"].(string)
|
|
if login == "" {
|
|
return fmt.Errorf("无法确定当前用户")
|
|
}
|
|
userID, _ := userData["user_id"].(float64)
|
|
body := map[string]interface{}{
|
|
"name": name,
|
|
"repository_name": name,
|
|
"user_id": int(userID),
|
|
}
|
|
if desc := ctx.Arg("description"); desc != "" {
|
|
body["description"] = desc
|
|
}
|
|
if ctx.Arg("private") == "true" {
|
|
body["private"] = true
|
|
}
|
|
env, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s", login, name), body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "fork",
|
|
Description: tr.T("cmd.repo.fork.short"),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/forks", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "delete",
|
|
Description: tr.T("cmd.repo.delete.short"),
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("DELETE", ctx.RepoPath(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "languages",
|
|
Description: "Show language breakdown of a repository",
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "contributors",
|
|
Description: "List contributors of a repository",
|
|
Flags: []common.Flag{
|
|
{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"))
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/contributors", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "files",
|
|
Description: "List files in a repository directory",
|
|
Flags: []common.Flag{
|
|
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
|
{Name: "path", Short: "p", Usage: "Directory path (default: repository root)"},
|
|
},
|
|
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)
|
|
}
|
|
if p := ctx.Arg("path"); p != "" {
|
|
q.Set("filepath", p)
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "tags",
|
|
Description: "List tags of a repository",
|
|
Flags: []common.Flag{
|
|
{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"))
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
{
|
|
Name: "commits",
|
|
Description: "List commits of a repository",
|
|
Flags: []common.Flag{
|
|
{Name: "sha", Short: "s", Usage: "Branch name, tag, or commit SHA"},
|
|
{Name: "path", Short: "p", Usage: "Filter commits by file path"},
|
|
{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 sha := ctx.Arg("sha"); sha != "" {
|
|
q.Set("sha", sha)
|
|
}
|
|
if p := ctx.Arg("path"); p != "" {
|
|
q.Set("path", p)
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/commits", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|
if len(translators) > 0 && translators[0] != nil {
|
|
return translators[0]
|
|
}
|
|
return i18n.Default()
|
|
}
|
|
|
|
func runFiles(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
setRepoQueryIfPresent(q, "search", ctx.Arg("search"))
|
|
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runCommits(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
|
|
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
|
|
setRepoQueryIfPresent(q, "sha", ctx.Arg("ref"))
|
|
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/commits", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runCommitFiles(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
sha, err := ctx.RequireArg("sha")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
if file := strings.TrimSpace(ctx.Arg("file")); file != "" {
|
|
q.Set("filepath", file)
|
|
} else {
|
|
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
|
|
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1%s/commits/%s/files", ctx.RepoPath(), url.PathEscape(sha)), q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runCommitDiff(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
sha, err := ctx.RequireArg("sha")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/commits/%s/diff", ctx.RepoPath(), url.PathEscape(sha)), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runTags(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
if name := strings.TrimSpace(ctx.Arg("name")); name != "" {
|
|
q.Set("name", name)
|
|
}
|
|
onlyName := strings.TrimSpace(ctx.Arg("only-name"))
|
|
if onlyName != "" && onlyName != "false" {
|
|
q.Set("only_name", onlyName)
|
|
}
|
|
if len(q) > 0 {
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
|
|
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
|
|
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/tags", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runTag(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
name, err := ctx.RequireArg("name")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runDeleteTag(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
name, err := ctx.RequireArg("name")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name))
|
|
if ctx.Arg("dry-run") == "true" {
|
|
return ctx.OutputData(map[string]interface{}{
|
|
"dry_run": true,
|
|
"action": "delete_tag",
|
|
"method": "DELETE",
|
|
"path": path,
|
|
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
|
"tag": name,
|
|
})
|
|
}
|
|
if ctx.Arg("yes") != "true" {
|
|
return fmt.Errorf("tag deletion is destructive; run --dry-run first, then pass --yes to execute")
|
|
}
|
|
env, err := ctx.CallAPI("DELETE", path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runBatchCommit(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
payload, err := batchCommitPayload(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := "/v1" + ctx.RepoPath() + "/contents/batch"
|
|
if ctx.Arg("dry-run") == "true" {
|
|
return ctx.OutputData(map[string]interface{}{
|
|
"dry_run": true,
|
|
"action": "batch_commit",
|
|
"method": "POST",
|
|
"path": path,
|
|
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
|
"payload": payload,
|
|
})
|
|
}
|
|
if ctx.Arg("yes") != "true" {
|
|
return fmt.Errorf("batch file commit changes repository content; run --dry-run first, then pass --yes to execute")
|
|
}
|
|
env, err := ctx.CallAPI("POST", path, payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runLanguages(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runContributors(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/contributors", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runContributorStats(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
|
|
if passYear := ctx.Arg("pass-year"); strings.TrimSpace(passYear) != "" {
|
|
year, err := parseRepoPositiveInt(passYear, "pass-year")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
q.Set("pass_year", strconv.Itoa(year))
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/contributors/stat", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runCodeStats(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q := url.Values{}
|
|
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
|
|
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/code_stats", q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runCommunityList(ctx *common.RuntimeContext, path string) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
q, err := communityTimeRangeQuery(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
q.Set("page", ctx.Arg("page"))
|
|
q.Set("limit", ctx.Arg("limit"))
|
|
if ctx.Arg("all") == "true" {
|
|
items, err := ctx.PaginateAllKey(ctx.RepoPath()+"/"+path, q, "users")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(common.NewListEnvelope("users", items))
|
|
}
|
|
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/"+path, q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func communityListFlags() []common.Flag {
|
|
return []common.Flag{
|
|
{Name: "start-at", Usage: "Start timestamp"},
|
|
{Name: "end-at", Usage: "End timestamp"},
|
|
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
|
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
|
{Name: "all", Usage: "Fetch all pages automatically (ignores --page)", Bool: true, Default: "false"},
|
|
}
|
|
}
|
|
|
|
func repoInteractionFlags() []common.Flag {
|
|
return []common.Flag{
|
|
{Name: "project-id", Usage: "GitLink project ID. If omitted, it is resolved from --owner/--repo."},
|
|
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
|
|
}
|
|
}
|
|
|
|
func runRepoFollowAction(ctx *common.RuntimeContext, method, path, action string) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
projectID, err := resolveRepoProjectID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
query := url.Values{}
|
|
query.Set("target_type", "project")
|
|
query.Set("id", projectID)
|
|
if ctx.Arg("dry-run") == "true" {
|
|
return repoInteractionDryRun(ctx, method, path, action, projectID, query)
|
|
}
|
|
env, err := ctx.CallAPIWithQuery(method, path, query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func runRepoPraiseAction(ctx *common.RuntimeContext, method, action string) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
projectID, err := resolveRepoProjectID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := fmt.Sprintf("/projects/%s/praise_tread/%s", projectID, action)
|
|
if ctx.Arg("dry-run") == "true" {
|
|
return repoInteractionDryRun(ctx, method, path, action, projectID, nil)
|
|
}
|
|
env, err := ctx.CallAPI(method, path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(env)
|
|
}
|
|
|
|
func repoInteractionDryRun(ctx *common.RuntimeContext, method, path, action, projectID string, query url.Values) error {
|
|
preview := map[string]interface{}{
|
|
"dry_run": true,
|
|
"action": action,
|
|
"method": method,
|
|
"path": path,
|
|
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
|
"project_id": projectID,
|
|
}
|
|
if len(query) > 0 {
|
|
preview["query"] = query.Encode()
|
|
}
|
|
return ctx.OutputData(preview)
|
|
}
|
|
|
|
func resolveRepoProjectID(ctx *common.RuntimeContext) (string, error) {
|
|
if raw := strings.TrimSpace(ctx.Arg("project-id")); raw != "" {
|
|
return normalizeRepoProjectID(raw)
|
|
}
|
|
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{"id", "project_id"} {
|
|
if id := repoProjectIDString(data[key]); id != "" {
|
|
return id, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("resolve project id: repository response did not include id")
|
|
}
|
|
|
|
func normalizeRepoProjectID(value string) (string, error) {
|
|
value = strings.TrimSpace(value)
|
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return "", fmt.Errorf("invalid --project-id %q: use a positive numeric project ID", value)
|
|
}
|
|
return strconv.FormatInt(parsed, 10), nil
|
|
}
|
|
|
|
func repoProjectIDString(value interface{}) string {
|
|
switch v := value.(type) {
|
|
case float64:
|
|
if v > 0 {
|
|
return strconv.FormatInt(int64(v), 10)
|
|
}
|
|
case int:
|
|
if v > 0 {
|
|
return strconv.Itoa(v)
|
|
}
|
|
case int64:
|
|
if v > 0 {
|
|
return strconv.FormatInt(v, 10)
|
|
}
|
|
case string:
|
|
id, err := normalizeRepoProjectID(v)
|
|
if err == nil {
|
|
return id
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func communityTimeRangeQuery(ctx *common.RuntimeContext) (url.Values, error) {
|
|
q := url.Values{}
|
|
start, hasStart, err := parseOptionalRepoNonNegativeInt(ctx.Arg("start-at"), "start-at")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end, hasEnd, err := parseOptionalRepoNonNegativeInt(ctx.Arg("end-at"), "end-at")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasStart {
|
|
q.Set("start_at", strconv.Itoa(start))
|
|
}
|
|
if hasEnd {
|
|
q.Set("end_at", strconv.Itoa(end))
|
|
}
|
|
if hasStart && hasEnd && start > end {
|
|
return nil, fmt.Errorf("--start-at must be less than or equal to --end-at")
|
|
}
|
|
return q, nil
|
|
}
|
|
|
|
func setRepoQueryIfPresent(q url.Values, key, value string) {
|
|
if value := strings.TrimSpace(value); value != "" {
|
|
q.Set(key, value)
|
|
}
|
|
}
|
|
|
|
func firstRepoValue(value, fallback string) string {
|
|
if value := strings.TrimSpace(value); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func batchCommitPayload(ctx *common.RuntimeContext) (map[string]interface{}, error) {
|
|
branch, err := ctx.RequireArg("branch")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message, err := ctx.RequireArg("message")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawFiles, err := ctx.RequireArg("files")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
encoding := firstRepoValue(ctx.Arg("encoding"), "text")
|
|
if encoding != "text" && encoding != "base64" {
|
|
return nil, fmt.Errorf("invalid --encoding %q: use text or base64", encoding)
|
|
}
|
|
files, err := parseBatchFileSpecs(rawFiles, encoding)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload := map[string]interface{}{
|
|
"branch": strings.TrimSpace(branch),
|
|
"message": message,
|
|
"files": files,
|
|
}
|
|
if newBranch := strings.TrimSpace(ctx.Arg("new-branch")); newBranch != "" {
|
|
payload["new_branch"] = newBranch
|
|
}
|
|
setRepoPayloadIfPresent(payload, "author_name", ctx.Arg("author-name"))
|
|
setRepoPayloadIfPresent(payload, "author_email", ctx.Arg("author-email"))
|
|
setRepoPayloadIfPresent(payload, "committer_name", ctx.Arg("committer-name"))
|
|
setRepoPayloadIfPresent(payload, "committer_email", ctx.Arg("committer-email"))
|
|
return payload, nil
|
|
}
|
|
|
|
func parseBatchFileSpecs(raw, encoding string) ([]map[string]interface{}, error) {
|
|
specs := strings.Split(raw, ";")
|
|
files := make([]map[string]interface{}, 0, len(specs))
|
|
for _, spec := range specs {
|
|
spec = strings.TrimSpace(spec)
|
|
if spec == "" {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(spec, ":", 3)
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("invalid --files item %q: use action:path[:content]", spec)
|
|
}
|
|
action := strings.TrimSpace(parts[0])
|
|
path := strings.TrimSpace(parts[1])
|
|
if !isBatchFileAction(action) {
|
|
return nil, fmt.Errorf("invalid file action %q: use create, update, or delete", action)
|
|
}
|
|
if path == "" {
|
|
return nil, fmt.Errorf("invalid --files item %q: file path is required", spec)
|
|
}
|
|
item := map[string]interface{}{
|
|
"action_type": action,
|
|
"file_path": path,
|
|
}
|
|
if action != "delete" {
|
|
if len(parts) != 3 {
|
|
return nil, fmt.Errorf("file action %q for %q requires content", action, path)
|
|
}
|
|
item["content"] = parts[2]
|
|
item["encoding"] = encoding
|
|
}
|
|
files = append(files, item)
|
|
}
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("--files must include at least one file operation")
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func isBatchFileAction(action string) bool {
|
|
switch action {
|
|
case "create", "update", "delete":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func setRepoPayloadIfPresent(payload map[string]interface{}, key, value string) {
|
|
if value := strings.TrimSpace(value); value != "" {
|
|
payload[key] = value
|
|
}
|
|
}
|
|
|
|
func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) {
|
|
if strings.TrimSpace(value) == "" {
|
|
return 0, false, nil
|
|
}
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
|
if err != nil || parsed < 0 {
|
|
return 0, false, fmt.Errorf("invalid --%s %q: use a non-negative integer", name, value)
|
|
}
|
|
return parsed, true, nil
|
|
}
|
|
|
|
func parseRepoPositiveInt(value, name string) (int, error) {
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
|
if err != nil || parsed <= 0 {
|
|
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", name, value)
|
|
}
|
|
return parsed, nil
|
|
}
|