feat: merge jtx_branch unique commands into master
CI / Build, Lint, Test (push) Failing after 13m10s Details

- 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 <noreply@anthropic.com>
This commit is contained in:
蒋天翔 2026-06-04 08:33:04 +08:00
parent 044551dbc8
commit 298c29795e
7 changed files with 459 additions and 172 deletions

View File

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

View File

@ -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
}

View File

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

View File

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

View File

@ -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{}

View File

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

View File

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