From 298c29795e28bb2a09672bd62db6cde7cdaa88dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E5=A4=A9=E7=BF=94?= Date: Thu, 4 Jun 2026 08:33:04 +0800 Subject: [PATCH] feat: merge jtx_branch unique commands into master - ci: add activate, deactivate, authorize commands; fix indentation - org: add teams, create-team, remove-user commands; fix indentation - search: add code, issues commands; fix indentation and cleanup - repo: add raw, activity, compare, create-file, update-file commands - notification: add watch command - label: add clone command - register_test: fix expected group count Co-Authored-By: Claude Opus 4.8 --- shortcuts/ci/ci.go | 78 ++++++------ shortcuts/label/label.go | 100 ++++++++++++++++ shortcuts/notification/notification.go | 29 +++++ shortcuts/org/org.go | 126 ++++++++++---------- shortcuts/register_test.go | 1 + shortcuts/repo/repo.go | 159 ++++++++++++++++++++++++- shortcuts/search/search.go | 138 ++++++++++----------- 7 files changed, 459 insertions(+), 172 deletions(-) diff --git a/shortcuts/ci/ci.go b/shortcuts/ci/ci.go index 0cd3b14..02de0d5 100644 --- a/shortcuts/ci/ci.go +++ b/shortcuts/ci/ci.go @@ -95,47 +95,47 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { } return ctx.Output(env) }, - { - Name: "activate", - Description: "为仓库激活 CI/CD 功能", - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/activate", nil) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "activate", + Description: "为仓库激活 CI/CD 功能", + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/activate", nil) + if err != nil { + return err + } + return ctx.Output(env) }, - { - Name: "deactivate", - Description: "停用仓库的 CI/CD 功能", - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - env, err := ctx.CallAPI("DELETE", ctx.RepoPath()+"/deactivate", nil) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "deactivate", + Description: "停用仓库的 CI/CD 功能", + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", ctx.RepoPath()+"/deactivate", nil) + if err != nil { + return err + } + return ctx.Output(env) }, - { - Name: "authorize", - Description: "检查仓库的 CI/CD 授权状态", - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/ci_authorize", nil) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "authorize", + Description: "检查仓库的 CI/CD 授权状态", + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/ci_authorize", nil) + if err != nil { + return err + } + return ctx.Output(env) }, }, } diff --git a/shortcuts/label/label.go b/shortcuts/label/label.go index 2b6a298..6dd847e 100644 --- a/shortcuts/label/label.go +++ b/shortcuts/label/label.go @@ -91,6 +91,16 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "clone", + Description: "Clone labels from another repository", + Flags: []common.Flag{ + {Name: "source-owner", Short: "o", Usage: "Source repository owner", Required: true}, + {Name: "source-repo", Short: "r", Usage: "Source repository name", Required: true}, + {Name: "overwrite", Usage: "Overwrite existing labels with same name (true/false)", Default: "false"}, + }, + Run: runClone, + }, } } @@ -242,3 +252,93 @@ func firstNonEmpty(values ...string) string { } return "" } + +func runClone(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + sourceOwner, err := ctx.RequireArg("source-owner") + if err != nil { + return err + } + sourceRepo, err := ctx.RequireArg("source-repo") + if err != nil { + return err + } + + sourcePath := fmt.Sprintf("/v1/%s/%s/issue_tags", sourceOwner, sourceRepo) + env, err := ctx.CallAPI("GET", sourcePath, nil) + if err != nil { + return fmt.Errorf("failed to fetch source labels: %w", err) + } + data, ok := env.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected response format from source repository") + } + rawTags, ok := data["issue_tags"].([]interface{}) + if !ok { + return fmt.Errorf("no issue_tags found in source repository") + } + + overwrite := ctx.Arg("overwrite") == "true" + created := 0 + skipped := 0 + + for _, raw := range rawTags { + tag, ok := raw.(map[string]interface{}) + if !ok { + continue + } + name := stringFromMap(tag, "name") + if name == "" { + continue + } + if !overwrite { + if existing, _ := fetchLabelByName(ctx, name); existing != nil { + skipped++ + continue + } + } + color := stringFromMap(tag, "color") + if color == "" { + color = defaultLabelColor + } + payload := map[string]interface{}{ + "name": name, + "description": stringFromMap(tag, "description"), + "color": color, + } + if _, err := ctx.CallAPI("POST", labelPath(ctx), payload); err != nil { + return fmt.Errorf("failed to create label %q: %w", name, err) + } + created++ + } + + fmt.Printf("Cloned %d labels from %s/%s (skipped %d existing)\n", created, sourceOwner, sourceRepo, skipped) + return nil +} + +func fetchLabelByName(ctx *common.RuntimeContext, name string) (map[string]interface{}, error) { + env, err := ctx.CallAPI("GET", labelPath(ctx), nil) + if err != nil { + return nil, err + } + data, ok := env.Data.(map[string]interface{}) + if !ok { + return nil, nil + } + rawTags, ok := data["issue_tags"].([]interface{}) + if !ok { + return nil, nil + } + for _, raw := range rawTags { + tag, ok := raw.(map[string]interface{}) + if !ok { + continue + } + if stringFromMap(tag, "name") == name { + return tag, nil + } + } + return nil, nil +} diff --git a/shortcuts/notification/notification.go b/shortcuts/notification/notification.go index 485c73d..ac58be8 100644 --- a/shortcuts/notification/notification.go +++ b/shortcuts/notification/notification.go @@ -64,5 +64,34 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "watch", + Description: "关注或取消关注仓库的通知", + Flags: []common.Flag{ + {Name: "owner", Short: "o", Usage: "仓库所有者", Required: true}, + {Name: "repo", Short: "r", Usage: "仓库名称", Required: true}, + {Name: "unwatch", Usage: "取消关注(默认为关注)", Bool: true, Default: "false"}, + }, + Run: func(ctx *common.RuntimeContext) error { + owner, err := ctx.RequireArg("owner") + if err != nil { + return err + } + repo, err := ctx.RequireArg("repo") + if err != nil { + return err + } + path := fmt.Sprintf("/watchers/%s/%s.json", owner, repo) + method := "POST" + if ctx.Arg("unwatch") == "true" { + method = "DELETE" + } + env, err := ctx.CallAPI(method, path, nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } diff --git a/shortcuts/org/org.go b/shortcuts/org/org.go index 1003b6b..fde66ba 100644 --- a/shortcuts/org/org.go +++ b/shortcuts/org/org.go @@ -85,71 +85,71 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { } return ctx.Output(env) }, - { - Name: "teams", - Description: "列出组织下的所有团队", - Flags: []common.Flag{ - {Name: "id", Usage: "组织 ID", Required: true}, - }, - Run: func(ctx *common.RuntimeContext) error { - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s/teams", id), nil) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "teams", + Description: "列出组织下的所有团队", + Flags: []common.Flag{ + {Name: "id", Usage: "组织 ID", Required: true}, }, - { - Name: "create-team", - Description: "在组织下创建新团队", - Flags: []common.Flag{ - {Name: "id", Usage: "组织 ID", Required: true}, - {Name: "name", Short: "n", Usage: "团队名称", Required: true}, - }, - Run: func(ctx *common.RuntimeContext) error { - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - name, err := ctx.RequireArg("name") - if err != nil { - return err - } - body := map[string]interface{}{"name": name} - env, err := ctx.CallAPI("POST", fmt.Sprintf("/organizations/%s/teams", id), body) - if err != nil { - return err - } - return ctx.Output(env) - }, + Run: func(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s/teams", id), nil) + if err != nil { + return err + } + return ctx.Output(env) }, - { - Name: "remove-user", - Description: "从组织中移除成员", - Flags: []common.Flag{ - {Name: "id", Usage: "组织 ID", Required: true}, - {Name: "user", Short: "u", Usage: "要移除的用户 ID", Required: true}, - }, - Run: func(ctx *common.RuntimeContext) error { - orgID, err := ctx.RequireArg("id") - if err != nil { - return err - } - userID, err := ctx.RequireArg("user") - if err != nil { - return err - } - path := fmt.Sprintf("/organizations/%s/organization_users/%s", orgID, userID) - env, err := ctx.CallAPI("DELETE", path, nil) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "create-team", + Description: "在组织下创建新团队", + Flags: []common.Flag{ + {Name: "id", Usage: "组织 ID", Required: true}, + {Name: "name", Short: "n", Usage: "团队名称", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + name, err := ctx.RequireArg("name") + if err != nil { + return err + } + body := map[string]interface{}{"name": name} + env, err := ctx.CallAPI("POST", fmt.Sprintf("/organizations/%s/teams", id), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "remove-user", + Description: "从组织中移除成员", + Flags: []common.Flag{ + {Name: "id", Usage: "组织 ID", Required: true}, + {Name: "user", Short: "u", Usage: "要移除的用户 ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + orgID, err := ctx.RequireArg("id") + if err != nil { + return err + } + userID, err := ctx.RequireArg("user") + if err != nil { + return err + } + path := fmt.Sprintf("/organizations/%s/organization_users/%s", orgID, userID) + env, err := ctx.CallAPI("DELETE", path, nil) + if err != nil { + return err + } + return ctx.Output(env) }, }, } diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index e3f1351..49f4dc1 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -14,6 +14,7 @@ func TestRegisterAll(t *testing.T) { "repo", "issue", "label", "pr", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", + "notification", "wiki", "export", "pm", } groupSet := map[string]bool{} diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 93cbec8..a235ecf 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -92,7 +92,6 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { 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("failed to get current user: %w", err) @@ -259,6 +258,164 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "raw", + Description: "Get raw file content from a repository", + Flags: []common.Flag{ + {Name: "filepath", Short: "f", Usage: "Path to the file", Required: true}, + {Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA (default: default branch)"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + filepath, err := ctx.RequireArg("filepath") + 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()+"/raw", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "activity", + Description: "List recent activity/events 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()+"/activity", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "compare", + Description: "Compare two commits, branches, or tags", + Flags: []common.Flag{ + {Name: "from", Short: "f", Usage: "Base ref (branch/tag/SHA)", Required: true}, + {Name: "to", Short: "t", Usage: "Head ref (branch/tag/SHA)", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + from, err := ctx.RequireArg("from") + if err != nil { + return err + } + to, err := ctx.RequireArg("to") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/compare/%s...%s", ctx.RepoPath(), from, to), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "create-file", + Description: "Create a new file in a repository", + Flags: []common.Flag{ + {Name: "filepath", Short: "f", Usage: "Path for the new file", Required: true}, + {Name: "content", Short: "c", Usage: "File content (base64 or plain text)", Required: true}, + {Name: "message", Short: "m", Usage: "Commit message", Required: true}, + {Name: "branch", Short: "b", Usage: "Target branch (default: default branch)"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + filepath, err := ctx.RequireArg("filepath") + if err != nil { + return err + } + content, err := ctx.RequireArg("content") + if err != nil { + return err + } + message, err := ctx.RequireArg("message") + if err != nil { + return err + } + body := map[string]interface{}{ + "filepath": filepath, + "content": content, + "message": message, + } + if branch := ctx.Arg("branch"); branch != "" { + body["branch"] = branch + } + env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/contents", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "update-file", + Description: "Update an existing file in a repository", + Flags: []common.Flag{ + {Name: "filepath", Short: "f", Usage: "Path of the file to update", Required: true}, + {Name: "content", Short: "c", Usage: "New file content (base64 or plain text)", Required: true}, + {Name: "message", Short: "m", Usage: "Commit message", Required: true}, + {Name: "sha", Short: "s", Usage: "SHA of the file being replaced"}, + {Name: "branch", Short: "b", Usage: "Target branch (default: default branch)"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + filepath, err := ctx.RequireArg("filepath") + if err != nil { + return err + } + content, err := ctx.RequireArg("content") + if err != nil { + return err + } + message, err := ctx.RequireArg("message") + if err != nil { + return err + } + body := map[string]interface{}{ + "filepath": filepath, + "content": content, + "message": message, + } + if sha := ctx.Arg("sha"); sha != "" { + body["sha"] = sha + } + if branch := ctx.Arg("branch"); branch != "" { + body["branch"] = branch + } + env, err := ctx.CallAPI("PUT", ctx.RepoPath()+"/contents", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } diff --git a/shortcuts/search/search.go b/shortcuts/search/search.go index ab81f91..b7f70e0 100644 --- a/shortcuts/search/search.go +++ b/shortcuts/search/search.go @@ -52,77 +52,77 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { } return ctx.Output(env) }, - { - Name: "code", - Description: "在仓库中搜索代码", - Flags: []common.Flag{ - {Name: "keyword", Short: "k", Usage: "搜索关键词", Required: true}, - {Name: "owner", Usage: "仓库所有者(可选,限定范围)"}, - {Name: "repo", Usage: "仓库名称(可选,限定范围)"}, - {Name: "language", Usage: "编程语言过滤(如 go, python)"}, - {Name: "page", Short: "p", Usage: "页码", Default: "1"}, - {Name: "limit", Short: "l", Usage: "每页数量", Default: "20"}, - }, - Run: func(ctx *common.RuntimeContext) error { - keyword, err := ctx.RequireArg("keyword") - if err != nil { - return err - } - q := url.Values{} - q.Set("keyword", keyword) - q.Set("page", ctx.Arg("page")) - q.Set("limit", ctx.Arg("limit")) - if lang := ctx.Arg("language"); lang != "" { - q.Set("language", lang) - } - path := "/search/code" - if owner := ctx.Arg("owner"); owner != "" { - if repo := ctx.Arg("repo"); repo != "" { - path = fmt.Sprintf("/%s/%s/search/code", owner, repo) - } - } - env, err := ctx.CallAPIWithQuery("GET", path, q) - if err != nil { - return err - } - return ctx.Output(env) - }, + }, + { + Name: "code", + Description: "在仓库中搜索代码", + Flags: []common.Flag{ + {Name: "keyword", Short: "k", Usage: "搜索关键词", Required: true}, + {Name: "owner", Usage: "仓库所有者(可选,限定范围)"}, + {Name: "repo", Usage: "仓库名称(可选,限定范围)"}, + {Name: "language", Usage: "编程语言过滤(如 go, python)"}, + {Name: "page", Short: "p", Usage: "页码", Default: "1"}, + {Name: "limit", Short: "l", Usage: "每页数量", Default: "20"}, }, - { - Name: "issues", - Description: "搜索 Issue", - Flags: []common.Flag{ - {Name: "keyword", Short: "k", Usage: "搜索关键词", Required: true}, - {Name: "state", Short: "s", Usage: "状态过滤: open/closed/all", Default: "all"}, - {Name: "label", Usage: "标签过滤"}, - {Name: "author", Usage: "作者过滤"}, - {Name: "page", Short: "p", Usage: "页码", Default: "1"}, - {Name: "limit", Short: "l", Usage: "每页数量", Default: "20"}, - }, - Run: func(ctx *common.RuntimeContext) error { - keyword, err := ctx.RequireArg("keyword") - if err != nil { - return err + Run: func(ctx *common.RuntimeContext) error { + keyword, err := ctx.RequireArg("keyword") + if err != nil { + return err + } + q := url.Values{} + q.Set("keyword", keyword) + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + if lang := ctx.Arg("language"); lang != "" { + q.Set("language", lang) + } + path := "/search/code" + if owner := ctx.Arg("owner"); owner != "" { + if repo := ctx.Arg("repo"); repo != "" { + path = fmt.Sprintf("/%s/%s/search/code", owner, repo) } - q := url.Values{} - q.Set("keyword", keyword) - q.Set("page", ctx.Arg("page")) - q.Set("limit", ctx.Arg("limit")) - if state := ctx.Arg("state"); state != "" && state != "all" { - q.Set("state", state) - } - if label := ctx.Arg("label"); label != "" { - q.Set("label", label) - } - if author := ctx.Arg("author"); author != "" { - q.Set("author", author) - } - env, err := ctx.CallAPIWithQuery("GET", "/search/issues", q) - if err != nil { - return err - } - return ctx.Output(env) - }, + } + env, err := ctx.CallAPIWithQuery("GET", path, q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "issues", + Description: "搜索 Issue", + Flags: []common.Flag{ + {Name: "keyword", Short: "k", Usage: "搜索关键词", Required: true}, + {Name: "state", Short: "s", Usage: "状态过滤: open/closed/all", Default: "all"}, + {Name: "label", Usage: "标签过滤"}, + {Name: "author", Usage: "作者过滤"}, + {Name: "page", Short: "p", Usage: "页码", Default: "1"}, + {Name: "limit", Short: "l", Usage: "每页数量", Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + keyword, err := ctx.RequireArg("keyword") + if err != nil { + return err + } + q := url.Values{} + q.Set("keyword", keyword) + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + if state := ctx.Arg("state"); state != "" && state != "all" { + q.Set("state", state) + } + if label := ctx.Arg("label"); label != "" { + q.Set("label", label) + } + if author := ctx.Arg("author"); author != "" { + q.Set("author", author) + } + env, err := ctx.CallAPIWithQuery("GET", "/search/issues", q) + if err != nil { + return err + } + return ctx.Output(env) }, }, }