forked from Gitlink/gitlink-cli
Merge branch 'master' of https://gitlink.org.cn/chroe/gitlink-cli
This commit is contained in:
commit
d072d54995
|
|
@ -10,6 +10,7 @@ import (
|
|||
authCmd "github.com/gitlink-org/gitlink-cli/cmd/auth"
|
||||
apiCmd "github.com/gitlink-org/gitlink-cli/cmd/api"
|
||||
configCmd "github.com/gitlink-org/gitlink-cli/cmd/config"
|
||||
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts"
|
||||
)
|
||||
|
||||
|
|
@ -47,7 +48,10 @@ var versionCmd = &cobra.Command{
|
|||
|
||||
func Execute() error {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintf(os.Stderr, "错误: %s\n", err)
|
||||
if s := clierrors.FindSuggestion(err); s != "" {
|
||||
fmt.Fprintf(os.Stderr, "建议: %s\n", s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -24,12 +24,17 @@ type APIError struct {
|
|||
StatusCode int
|
||||
Code interface{}
|
||||
Message string
|
||||
Suggestion string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("[%v] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *APIError) Suggest() string {
|
||||
return e.Suggestion
|
||||
}
|
||||
|
||||
func New() (*Client, error) {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
|
|
@ -102,10 +107,12 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
|
||||
// Check HTTP-level errors
|
||||
if resp.StatusCode >= 400 {
|
||||
suggestion := suggestFix(resp.StatusCode)
|
||||
return nil, &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: resp.StatusCode,
|
||||
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
|
||||
Suggestion: suggestion,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +139,7 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
StatusCode: int(statusCode),
|
||||
Code: int(statusCode),
|
||||
Message: msg,
|
||||
Suggestion: suggestion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -258,14 +266,20 @@ func (c *Client) DoRaw(method, path string, body interface{}, query url.Values)
|
|||
|
||||
func suggestFix(code int) string {
|
||||
switch code {
|
||||
case 400:
|
||||
return "请求参数格式错误,请检查参数值是否正确"
|
||||
case 401:
|
||||
return "请先运行 gitlink-cli auth login 登录"
|
||||
case 403:
|
||||
return "权限不足,请确认账户权限或联系项目管理员"
|
||||
case 404:
|
||||
return "资源不存在,请检查 owner/repo/id 是否正确"
|
||||
case 409:
|
||||
return "资源冲突,可能存在同名资源"
|
||||
case 422:
|
||||
return "参数校验失败,请检查请求参数"
|
||||
case 500, 502, 503:
|
||||
return "服务器内部错误,请稍后重试或联系平台管理员"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func ResolveOwnerRepo(flagOwner, flagRepo string) (string, string, error) {
|
|||
owner, repo, err := fromGitRemote()
|
||||
if err != nil {
|
||||
if flagOwner == "" || flagRepo == "" {
|
||||
return "", "", fmt.Errorf("cannot detect owner/repo from git remote: %w\nUse --owner and --repo flags to specify explicitly", err)
|
||||
return "", "", fmt.Errorf("无法从 git remote 自动检测 owner/repo: %w\n请使用 --owner 和 --repo 参数手动指定", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ func ResolveOwnerRepo(flagOwner, flagRepo string) (string, string, error) {
|
|||
func fromGitRemote() (string, string, error) {
|
||||
out, err := exec.Command("git", "remote", "get-url", "origin").Output()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("not a git repository or no remote 'origin'")
|
||||
return "", "", fmt.Errorf("当前目录不是 git 仓库或未配置 remote 'origin'")
|
||||
}
|
||||
remote := strings.TrimSpace(string(out))
|
||||
return parseRemoteURL(remote)
|
||||
|
|
@ -45,7 +45,7 @@ func parseRemoteURL(remote string) (string, string, error) {
|
|||
if strings.HasPrefix(remote, "git@") {
|
||||
parts := strings.SplitN(remote, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("cannot parse SSH remote: %s", remote)
|
||||
return "", "", fmt.Errorf("无法解析 SSH 远程地址: %s", remote)
|
||||
}
|
||||
return parsePathSegments(parts[1])
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ func parseRemoteURL(remote string) (string, string, error) {
|
|||
// HTTPS format: https://www.gitlink.org.cn/owner/repo.git
|
||||
u, err := url.Parse(remote)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot parse remote URL: %s", remote)
|
||||
return "", "", fmt.Errorf("无法解析远程地址 URL: %s", remote)
|
||||
}
|
||||
return parsePathSegments(u.Path)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func parsePathSegments(path string) (string, string, error) {
|
|||
path = strings.TrimSuffix(path, ".git")
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("cannot extract owner/repo from path: %s", path)
|
||||
return "", "", fmt.Errorf("无法从路径提取 owner/repo: %s", path)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Suggester is implemented by errors that carry a user-facing suggestion.
|
||||
type Suggester interface {
|
||||
Suggest() string
|
||||
}
|
||||
|
||||
// Error is a CLI error with an optional suggestion for the user.
|
||||
type Error struct {
|
||||
Message string
|
||||
Suggestion string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e *Error) Suggest() string {
|
||||
return e.Suggestion
|
||||
}
|
||||
|
||||
// New creates a new Error with an optional suggestion.
|
||||
func New(message, suggestion string) *Error {
|
||||
return &Error{Message: message, Suggestion: suggestion}
|
||||
}
|
||||
|
||||
// Wrapf wraps an error with a formatted message and optional suggestion.
|
||||
func Wrapf(err error, suggestion, format string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Suggestion: suggestion,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap wraps an error with a message and optional suggestion.
|
||||
func Wrap(err error, message, suggestion string) *Error {
|
||||
return &Error{Message: message, Suggestion: suggestion, Err: err}
|
||||
}
|
||||
|
||||
// FindSuggestion walks the error chain and returns the first suggestion found.
|
||||
func FindSuggestion(err error) string {
|
||||
var s Suggester
|
||||
if errors.As(err, &s) {
|
||||
return s.Suggest()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -18,14 +18,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/branches", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取分支列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -39,9 +39,12 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
from := ctx.Arg("from")
|
||||
if from == "" {
|
||||
from = "master"
|
||||
|
|
@ -52,7 +55,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建分支失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -65,15 +68,18 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
payload := map[string]interface{}{
|
||||
"branch_name": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches/delete", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除分支失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -86,15 +92,18 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
payload := map[string]interface{}{
|
||||
"branch_name": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/protected_branches", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("设置分支保护失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -107,13 +116,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/protected_branches/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/protected_branches/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("取消分支保护失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/builds", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取构建列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -40,9 +40,12 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
build, err := ctx.RequireArg("build")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
stage := ctx.Arg("stage")
|
||||
step := ctx.Arg("step")
|
||||
if stage == "" {
|
||||
|
|
@ -53,7 +56,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/builds/%s/logs/%s/%s", ctx.RepoPath(), build, stage, step), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取构建日志失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -66,13 +69,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/builds/%s/restart", ctx.RepoPath(), build), nil)
|
||||
build, err := ctx.RequireArg("build")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/builds/%s/restart", ctx.RepoPath(), build), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("重启构建失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -84,13 +90,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/builds/%s/stop", ctx.RepoPath(), build), nil)
|
||||
build, err := ctx.RequireArg("build")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/builds/%s/stop", ctx.RepoPath(), build), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("停止构建失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -29,7 +29,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/commits", ctx.Owner, ctx.Repo), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取提交列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -44,7 +44,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
sha, err := ctx.RequireArg("sha")
|
||||
if err != nil {
|
||||
|
|
@ -55,7 +55,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/commits/%s/files", ctx.Owner, ctx.Repo, sha), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看提交详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -68,7 +68,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
sha, err := ctx.RequireArg("sha")
|
||||
if err != nil {
|
||||
|
|
@ -76,7 +76,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1/%s/%s/commits/%s/diff", ctx.Owner, ctx.Repo, sha), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取提交 Diff 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -90,7 +90,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
filePath, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
|
|
@ -101,7 +101,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("sha", ctx.Arg("sha"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/blame", ctx.Owner, ctx.Repo), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Blame 信息失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func newBatchCloseShortcut() *common.Shortcut {
|
|||
|
||||
func runBatchClose(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
|
@ -55,7 +55,7 @@ func runBatchClose(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
|
||||
return fmt.Errorf("未提供 Issue 编号,请使用 --numbers 1,2,3 或 --from issues.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
|
@ -92,7 +92,7 @@ func runBatchClose(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d issue(s) failed to close", summary.Failed, summary.Total)
|
||||
return fmt.Errorf("%d / %d 个 Issue 关闭失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ func runBatchClose(ctx *common.RuntimeContext) error {
|
|||
func closeIssue(ctx *common.RuntimeContext, number string) error {
|
||||
current, err := fetchExistingIssue(ctx, number)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch issue: %w", err)
|
||||
return fmt.Errorf("获取 Issue 详情: %w", err)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
|
|
@ -109,7 +109,7 @@ func closeIssue(ctx *common.RuntimeContext, number string) error {
|
|||
"status_id": closedIssueStatusID,
|
||||
}
|
||||
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil {
|
||||
return fmt.Errorf("close issue: %w", err)
|
||||
return fmt.Errorf("关闭 Issue: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ func newBatchAssignShortcut() *common.Shortcut {
|
|||
|
||||
func runBatchAssign(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
|
||||
user, err := ctx.RequireArg("user")
|
||||
|
|
@ -143,7 +143,7 @@ func runBatchAssign(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
|
||||
return fmt.Errorf("未提供 Issue 编号,请使用 --numbers 1,2,3 或 --from issues.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
|
@ -178,7 +178,7 @@ func runBatchAssign(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d issue(s) failed to assign", summary.Failed, summary.Total)
|
||||
return fmt.Errorf("%d / %d 个 Issue 分配失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -188,7 +188,7 @@ func assignIssue(ctx *common.RuntimeContext, number, user string) error {
|
|||
"assigned_to_id": user,
|
||||
}
|
||||
if _, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/issues/%s/assignees", v1RepoPath(ctx), number), body); err != nil {
|
||||
return fmt.Errorf("assign issue: %w", err)
|
||||
return fmt.Errorf("分配 Issue: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -210,13 +210,13 @@ func newBatchLabelShortcut() *common.Shortcut {
|
|||
|
||||
func runBatchLabel(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
|
||||
addIDs := ctx.Arg("add")
|
||||
removeIDs := ctx.Arg("remove")
|
||||
if addIDs == "" && removeIDs == "" {
|
||||
return fmt.Errorf("at least one of --add or --remove is required")
|
||||
return fmt.Errorf("至少需要指定 --add 或 --remove 中的一个")
|
||||
}
|
||||
|
||||
numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
|
||||
|
|
@ -224,7 +224,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
|
||||
return fmt.Errorf("未提供 Issue 编号,请使用 --numbers 1,2,3 或 --from issues.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
|
@ -262,7 +262,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d issue(s) failed to label", summary.Failed, summary.Total)
|
||||
return fmt.Errorf("%d / %d 个 Issue 标签操作失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -274,12 +274,12 @@ func labelIssue(ctx *common.RuntimeContext, number string, addLabels, removeLabe
|
|||
"tag_id": labelID,
|
||||
}
|
||||
if _, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/tags", v1RepoPath(ctx), number), body); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("add label %s: %v", labelID, err))
|
||||
errs = append(errs, fmt.Sprintf("添加标签 %s: %v", labelID, err))
|
||||
}
|
||||
}
|
||||
for _, labelID := range removeLabels {
|
||||
if _, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/tags/%s", v1RepoPath(ctx), number, labelID), nil); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("remove label %s: %v", labelID, err))
|
||||
errs = append(errs, fmt.Sprintf("移除标签 %s: %v", labelID, err))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
|
|
@ -304,7 +304,7 @@ func newBatchMilestoneShortcut() *common.Shortcut {
|
|||
|
||||
func runBatchMilestone(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
|
||||
milestoneID, err := ctx.RequireArg("milestone")
|
||||
|
|
@ -317,7 +317,7 @@ func runBatchMilestone(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
|
||||
return fmt.Errorf("未提供 Issue 编号,请使用 --numbers 1,2,3 或 --from issues.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
|
@ -352,7 +352,7 @@ func runBatchMilestone(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d issue(s) failed to set milestone", summary.Failed, summary.Total)
|
||||
return fmt.Errorf("%d / %d 个 Issue 设置里程碑失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -360,16 +360,16 @@ func runBatchMilestone(ctx *common.RuntimeContext) error {
|
|||
func setMilestone(ctx *common.RuntimeContext, number, milestoneID string) error {
|
||||
current, err := fetchExistingIssue(ctx, number)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch issue: %w", err)
|
||||
return fmt.Errorf("获取 Issue 详情: %w", err)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"subject": current.Subject,
|
||||
"description": current.Description,
|
||||
"subject": current.Subject,
|
||||
"description": current.Description,
|
||||
"fixed_version_id": milestoneID,
|
||||
}
|
||||
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil {
|
||||
return fmt.Errorf("set milestone: %w", err)
|
||||
return fmt.Errorf("设置里程碑: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -413,7 +413,7 @@ func parseIssueNumbers(value string) ([]string, error) {
|
|||
func readIssueNumbersFromCSV(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read issue numbers from CSV: %w", err)
|
||||
return nil, fmt.Errorf("读取 CSV 文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ func readIssueNumbersFromCSV(path string) ([]string, error) {
|
|||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse issue numbers from CSV: %w", err)
|
||||
return nil, fmt.Errorf("解析 CSV 文件失败: %w", err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
|
|
@ -459,7 +459,7 @@ func normalizeIssueNumbers(values []string) ([]string, error) {
|
|||
continue
|
||||
}
|
||||
if _, err := strconv.ParseInt(number, 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid issue number %q: issue numbers must be integers", number)
|
||||
return nil, fmt.Errorf("无效的 Issue 编号 %q: Issue 编号必须是整数", number)
|
||||
}
|
||||
if seen[number] {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -46,7 +46,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Issue 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -63,7 +63,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
|
|
@ -86,7 +86,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -99,7 +99,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
|
|
@ -107,7 +107,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看 Issue 详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -120,7 +120,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
|
|
@ -128,7 +128,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
current, err := fetchExistingIssue(ctx, number)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("关闭 Issue 失败: %w", err)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
|
|
@ -138,7 +138,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("关闭 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -154,7 +154,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
|
|
@ -164,12 +164,12 @@ func Shortcuts() []*common.Shortcut {
|
|||
description := ctx.Arg("body")
|
||||
state := ctx.Arg("state")
|
||||
if title == "" && description == "" && state == "" {
|
||||
return fmt.Errorf("at least one of --title, --body, or --state is required")
|
||||
return fmt.Errorf("至少需要指定 --title、--body 或 --state 中的一个")
|
||||
}
|
||||
|
||||
current, err := fetchExistingIssue(ctx, number)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新 Issue 失败: %w", err)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
|
|
@ -191,7 +191,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -205,16 +205,22 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := ctx.RequireArg("user")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
number, _ := ctx.RequireArg("number")
|
||||
user, _ := ctx.RequireArg("user")
|
||||
body := map[string]interface{}{
|
||||
"assigned_to_id": user,
|
||||
}
|
||||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/issues/%s/assignees", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("分配 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -229,13 +235,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
number, _ := ctx.RequireArg("number")
|
||||
addIDs := ctx.Arg("add")
|
||||
removeIDs := ctx.Arg("remove")
|
||||
if addIDs == "" && removeIDs == "" {
|
||||
return fmt.Errorf("at least one of --add or --remove is required")
|
||||
return fmt.Errorf("至少需要指定 --add 或 --remove 中的一个")
|
||||
}
|
||||
|
||||
var errs []string
|
||||
|
|
@ -249,7 +258,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
"tag_id": labelID,
|
||||
}
|
||||
if _, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/tags", v1RepoPath(ctx), number), body); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("add label %s: %v", labelID, err))
|
||||
errs = append(errs, fmt.Sprintf("添加标签 %s: %v", labelID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -260,12 +269,12 @@ func Shortcuts() []*common.Shortcut {
|
|||
continue
|
||||
}
|
||||
if _, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/tags/%s", v1RepoPath(ctx), number, labelID), nil); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("remove label %s: %v", labelID, err))
|
||||
errs = append(errs, fmt.Sprintf("移除标签 %s: %v", labelID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("label operations failed:\n%s", strings.Join(errs, "\n"))
|
||||
return fmt.Errorf("标签操作失败:\n%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "标签操作成功",
|
||||
|
|
@ -281,7 +290,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
|
|
@ -296,7 +305,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("添加评论失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -307,15 +316,15 @@ func Shortcuts() []*common.Shortcut {
|
|||
func fetchExistingIssue(ctx *common.RuntimeContext, number string) (*existingIssue, error) {
|
||||
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("获取 Issue 详情: %w", err)
|
||||
}
|
||||
issueData, ok := getEnv.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse issue data")
|
||||
return nil, fmt.Errorf("解析 Issue 数据失败")
|
||||
}
|
||||
subject, _ := issueData["subject"].(string)
|
||||
if subject == "" {
|
||||
return nil, fmt.Errorf("failed to parse issue subject")
|
||||
return nil, fmt.Errorf("解析 Issue 标题失败")
|
||||
}
|
||||
description, _ := issueData["description"].(string)
|
||||
return &existingIssue{
|
||||
|
|
@ -334,6 +343,6 @@ func normalizeIssueStatus(state string) (interface{}, error) {
|
|||
if id, err := strconv.Atoi(state); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid --state %q: use open, closed, or a numeric status_id", state)
|
||||
return nil, fmt.Errorf("无效的 --state 值 %q: 请使用 open、closed 或数字 status_id", state)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,8 +320,8 @@ func TestIssueAssignRequiresNumberAndUser(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "assign", map[string]string{"number": ""})
|
||||
if err != nil {
|
||||
t.Fatalf("assign shortcut should not error (follows existing pattern): %v", err)
|
||||
if err == nil {
|
||||
t.Fatal("assign shortcut should error when required flags are missing")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -33,7 +33,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issue_tags", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取标签列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -48,7 +48,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
|
|
@ -63,7 +63,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issue_tags", body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建标签失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -79,7 +79,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -89,7 +89,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
color := ctx.Arg("color")
|
||||
desc := ctx.Arg("description")
|
||||
if name == "" && color == "" && desc == "" {
|
||||
return fmt.Errorf("at least one of --name, --color, or --description is required")
|
||||
return fmt.Errorf("至少需要指定 --name、--color 或 --description 中的一个")
|
||||
}
|
||||
body := map[string]interface{}{}
|
||||
if name != "" {
|
||||
|
|
@ -103,7 +103,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issue_tags/%s", v1RepoPath(ctx), id), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新标签失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -116,7 +116,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -124,7 +124,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issue_tags/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除标签失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -45,7 +45,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/milestones", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取里程碑列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -58,7 +58,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -66,7 +66,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/milestones/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看里程碑详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -81,7 +81,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
|
|
@ -98,7 +98,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/milestones", body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建里程碑失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -114,7 +114,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -124,7 +124,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
desc := ctx.Arg("description")
|
||||
due := ctx.Arg("due")
|
||||
if name == "" && desc == "" && due == "" {
|
||||
return fmt.Errorf("at least one of --name, --description, or --due is required")
|
||||
return fmt.Errorf("至少需要指定 --name、--description 或 --due 中的一个")
|
||||
}
|
||||
body := map[string]interface{}{}
|
||||
if name != "" {
|
||||
|
|
@ -138,7 +138,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/milestones/%s", v1RepoPath(ctx), id), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新里程碑失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -151,7 +151,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -162,7 +162,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s/milestones/%s/update_status", ctx.Owner, ctx.Repo, id), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("关闭里程碑失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -175,7 +175,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -183,7 +183,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/milestones/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除里程碑失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type batchInviteResult struct {
|
||||
User string `json:"user" yaml:"user"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type batchInviteSummary struct {
|
||||
Org string `json:"org" yaml:"org"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Results []batchInviteResult `json:"results" yaml:"results"`
|
||||
}
|
||||
|
||||
func newBatchInviteShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-invite",
|
||||
Description: "批量邀请成员加入组织,支持逗号分隔列表或 CSV 文件",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "组织 ID 或 login", Required: true},
|
||||
{Name: "users", Short: "u", Usage: "逗号分隔的用户名或 ID,例如: alice,bob,charlie"},
|
||||
{Name: "from", Usage: "从 CSV 文件读取用户名。支持 user/login/user_id 列名或无表头首列"},
|
||||
{Name: "role", Short: "r", Usage: "成员角色: member 或 admin", Default: "member"},
|
||||
{Name: "dry-run", Usage: "仅预览将要邀请的成员,不实际执行", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchInvite,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchInvite(ctx *common.RuntimeContext) error {
|
||||
start := time.Now()
|
||||
|
||||
orgID, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role := ctx.Arg("role")
|
||||
if role == "" {
|
||||
role = "member"
|
||||
}
|
||||
if role != "member" && role != "admin" {
|
||||
return fmt.Errorf("无效的角色 %q: 必须为 member 或 admin", role)
|
||||
}
|
||||
|
||||
users, err := collectUsers(ctx.Arg("users"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return fmt.Errorf("未提供用户名,请使用 --users alice,bob 或 --from users.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
||||
summary := batchInviteSummary{
|
||||
Org: orgID,
|
||||
DryRun: dryRun,
|
||||
Total: len(users),
|
||||
Results: make([]batchInviteResult, 0, len(users)),
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
result := batchInviteResult{User: user, Action: "invite"}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"user_id": user,
|
||||
"role": role,
|
||||
}
|
||||
path := fmt.Sprintf("/organizations/%s/organization_users", orgID)
|
||||
if _, err := ctx.CallAPI("POST", path, body); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "invited"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
summary.Duration = time.Since(start).String()
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d / %d 个成员邀请失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectUsers(usersValue, csvPath string) ([]string, error) {
|
||||
users, err := parseUserList(usersValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if csvPath == "" {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
csvUsers, err := readUsersFromCSV(csvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mergeUserLists(users, csvUsers), nil
|
||||
}
|
||||
|
||||
func parseUserList(value string) ([]string, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return normalizeUserIDs(strings.Split(value, ","))
|
||||
}
|
||||
|
||||
func readUsersFromCSV(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 CSV 文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 CSV 文件失败: %w", err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userCol := -1
|
||||
startRow := 0
|
||||
for i, cell := range records[0] {
|
||||
switch strings.ToLower(strings.TrimSpace(cell)) {
|
||||
case "user", "login", "user_id", "username":
|
||||
userCol = i
|
||||
startRow = 1
|
||||
}
|
||||
}
|
||||
if userCol == -1 {
|
||||
userCol = 0
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(records)-startRow)
|
||||
for _, record := range records[startRow:] {
|
||||
if userCol >= len(record) {
|
||||
continue
|
||||
}
|
||||
values = append(values, record[userCol])
|
||||
}
|
||||
return normalizeUserIDs(values)
|
||||
}
|
||||
|
||||
func normalizeUserIDs(values []string) ([]string, error) {
|
||||
users := make([]string, 0, len(values))
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
user := strings.TrimSpace(value)
|
||||
if user == "" {
|
||||
continue
|
||||
}
|
||||
if seen[user] {
|
||||
continue
|
||||
}
|
||||
seen[user] = true
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func parseBool(value string) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(value), "true") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeUserLists(values ...[]string) []string {
|
||||
merged := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, users := range values {
|
||||
for _, u := range users {
|
||||
if seen[u] {
|
||||
continue
|
||||
}
|
||||
seen[u] = true
|
||||
merged = append(merged, u)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
newBatchInviteShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List organizations",
|
||||
|
|
@ -22,7 +23,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/organizations", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取组织列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -34,11 +35,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "id", Short: "i", Usage: "Organization ID or login", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s", id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s", id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看组织详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -51,13 +55,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
id, _ := ctx.RequireArg("id")
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/organizations/%s/organization_users", id), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取组织成员列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -70,7 +77,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "description", Short: "d", Usage: "Description"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"name": name,
|
||||
}
|
||||
|
|
@ -79,7 +89,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", "/organizations", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建组织失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -30,7 +30,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/pulls", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 PR 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -46,10 +46,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
head, err := ctx.RequireArg("head")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title, _ := ctx.RequireArg("title")
|
||||
head, _ := ctx.RequireArg("head")
|
||||
base := ctx.Arg("base")
|
||||
if base == "" {
|
||||
base = "master"
|
||||
|
|
@ -64,7 +70,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -77,13 +83,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看 PR 详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -96,9 +105,12 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
method := ctx.Arg("method")
|
||||
if method == "" {
|
||||
method = "merge"
|
||||
|
|
@ -108,7 +120,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/pr_merge", ctx.RepoPath(), id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("合并 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -121,13 +133,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("关闭 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -139,13 +154,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 PR 变更文件失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -157,13 +175,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 PR Diff 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -177,10 +198,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ctx.RequireArg("body")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
body, _ := ctx.RequireArg("body")
|
||||
action := ctx.Arg("action")
|
||||
if action == "" {
|
||||
action = "comment"
|
||||
|
|
@ -193,7 +220,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/reviews", ctx.RepoPath(), id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("提交 PR Review 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -207,14 +234,20 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ctx.RequireArg("body")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
body, _ := ctx.RequireArg("body")
|
||||
|
||||
prEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch PR: %w", err)
|
||||
return fmt.Errorf("获取 PR 详情: %w", err)
|
||||
}
|
||||
issueID, err := extractIssueID(prEnv)
|
||||
if err != nil {
|
||||
|
|
@ -226,7 +259,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/issues/%d/journals", ctx.Owner, ctx.Repo, issueID), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("添加 PR 评论失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -248,15 +281,15 @@ func mapAction(action string) string {
|
|||
func extractIssueID(env *output.Envelope) (int64, error) {
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected PR response format")
|
||||
return 0, fmt.Errorf("PR 响应格式异常")
|
||||
}
|
||||
issue, ok := data["issue"].(map[string]interface{})
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("PR response missing issue field")
|
||||
return 0, fmt.Errorf("PR 响应缺少 issue 字段")
|
||||
}
|
||||
idFloat, ok := issue["id"].(float64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("PR response missing issue.id field")
|
||||
return 0, fmt.Errorf("PR 响应缺少 issue.id 字段")
|
||||
}
|
||||
return int64(idFloat), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/releases", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Release 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -46,10 +46,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
tag, err := ctx.RequireArg("tag")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, _ := ctx.RequireArg("tag")
|
||||
name, _ := ctx.RequireArg("name")
|
||||
payload := map[string]interface{}{
|
||||
"tag_name": tag,
|
||||
"name": name,
|
||||
|
|
@ -65,7 +71,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/releases", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Release 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -78,13 +84,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看 Release 详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -98,24 +107,26 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
assetName := ctx.Arg("asset")
|
||||
dlDir := ctx.Arg("dir")
|
||||
if dlDir == "" {
|
||||
dlDir = "."
|
||||
}
|
||||
|
||||
// Fetch release to get assets
|
||||
releaseEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Release 信息失败: %w", err)
|
||||
}
|
||||
|
||||
releaseData, ok := releaseEnv.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected release response format")
|
||||
return fmt.Errorf("Release 响应格式异常")
|
||||
}
|
||||
|
||||
assets, ok := releaseData["assets"].([]interface{})
|
||||
|
|
@ -126,7 +137,6 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
|
||||
if assetName == "" {
|
||||
// List all assets
|
||||
var assetList []map[string]interface{}
|
||||
for _, a := range assets {
|
||||
asset, _ := a.(map[string]interface{})
|
||||
|
|
@ -140,7 +150,6 @@ func Shortcuts() []*common.Shortcut {
|
|||
})
|
||||
}
|
||||
|
||||
// Find and download the specific asset
|
||||
var downloadURL string
|
||||
for _, a := range assets {
|
||||
asset, _ := a.(map[string]interface{})
|
||||
|
|
@ -155,39 +164,37 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
|
||||
if downloadURL == "" {
|
||||
return fmt.Errorf("asset %q not found in release", assetName)
|
||||
return fmt.Errorf("Release 中未找到附件 %q", assetName)
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
if err := os.MkdirAll(dlDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dlDir, err)
|
||||
return fmt.Errorf("创建目录 %s 失败: %w", dlDir, err)
|
||||
}
|
||||
|
||||
// Download using authenticated client so private repos work
|
||||
resp, err := ctx.Client.HTTP.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download failed: %w", err)
|
||||
return fmt.Errorf("下载失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
||||
return fmt.Errorf("下载失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
destPath := filepath.Join(dlDir, assetName)
|
||||
file, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %s: %w", destPath, err)
|
||||
return fmt.Errorf("创建文件 %s 失败: %w", destPath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
written, err := io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": fmt.Sprintf("下载完成"),
|
||||
"message": "下载完成",
|
||||
"path": destPath,
|
||||
"size": written,
|
||||
}, nil))
|
||||
|
|
@ -201,22 +208,21 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if delErr != nil {
|
||||
// GitLink API bug: delete succeeds but returns error status.
|
||||
// Verify by checking if the release still exists.
|
||||
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if viewErr != nil {
|
||||
// Release no longer exists — delete actually succeeded
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "删除成功",
|
||||
}, nil))
|
||||
}
|
||||
// Release still exists — delete truly failed
|
||||
return delErr
|
||||
return fmt.Errorf("删除 Release 失败: %w", delErr)
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "删除成功",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type batchRepoResult struct {
|
||||
Repo string `json:"repo" yaml:"repo"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type batchRepoSummary struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Results []batchRepoResult `json:"results" yaml:"results"`
|
||||
}
|
||||
|
||||
func newBatchDeleteShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-delete",
|
||||
Description: "批量删除仓库,支持逗号分隔列表或 CSV 文件",
|
||||
Flags: []common.Flag{
|
||||
{Name: "repos", Short: "r", Usage: "逗号分隔的仓库标识,格式为 owner/repo,例如: alice/proj1,bob/proj2"},
|
||||
{Name: "from", Usage: "从 CSV 文件读取仓库标识。支持 owner/repo、owner,repo 双列或 owner/repo 单列格式"},
|
||||
{Name: "dry-run", Usage: "仅预览将要删除的仓库,不实际执行", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchDelete,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchDelete(ctx *common.RuntimeContext) error {
|
||||
start := time.Now()
|
||||
|
||||
repos, err := collectRepos(ctx.Arg("repos"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
return fmt.Errorf("未提供仓库标识,请使用 --repos owner/repo1,owner/repo2 或 --from repos.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
dryRunVal := false
|
||||
if ctx.Arg("dry-run") != "" {
|
||||
dryRunVal = dryRun
|
||||
}
|
||||
_ = dryRunVal
|
||||
|
||||
summary := batchRepoSummary{
|
||||
Total: len(repos),
|
||||
Results: make([]batchRepoResult, 0, len(repos)),
|
||||
}
|
||||
|
||||
for _, repoID := range repos {
|
||||
parts := strings.SplitN(repoID, "/", 2)
|
||||
result := batchRepoResult{Repo: repoID, Action: "delete"}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/%s", parts[0], parts[1])
|
||||
if _, err := ctx.CallAPI("DELETE", path, nil); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "deleted"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
summary.Duration = time.Since(start).String()
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d / %d 个仓库删除失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newBatchForkShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-fork",
|
||||
Description: "批量 Fork 仓库,支持逗号分隔列表或 CSV 文件",
|
||||
Flags: []common.Flag{
|
||||
{Name: "repos", Short: "r", Usage: "逗号分隔的仓库标识,格式为 owner/repo,例如: alice/proj1,bob/proj2"},
|
||||
{Name: "from", Usage: "从 CSV 文件读取仓库标识。支持 owner/repo、owner,repo 双列或 owner/repo 单列格式"},
|
||||
{Name: "dry-run", Usage: "仅预览将要 Fork 的仓库,不实际执行", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchFork,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchFork(ctx *common.RuntimeContext) error {
|
||||
start := time.Now()
|
||||
|
||||
repos, err := collectRepos(ctx.Arg("repos"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
return fmt.Errorf("未提供仓库标识,请使用 --repos owner/repo1,owner/repo2 或 --from repos.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
|
||||
summary := batchRepoSummary{
|
||||
Total: len(repos),
|
||||
Results: make([]batchRepoResult, 0, len(repos)),
|
||||
}
|
||||
|
||||
for _, repoID := range repos {
|
||||
parts := strings.SplitN(repoID, "/", 2)
|
||||
result := batchRepoResult{Repo: repoID, Action: "fork"}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/%s/forks", parts[0], parts[1])
|
||||
if _, err := ctx.CallAPI("POST", path, nil); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "forked"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
summary.Duration = time.Since(start).String()
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d / %d 个仓库 Fork 失败", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectRepos(reposValue, csvPath string) ([]string, error) {
|
||||
repos, err := parseRepoList(reposValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if csvPath == "" {
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
csvRepos, err := readReposFromCSV(csvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mergeRepoLists(repos, csvRepos), nil
|
||||
}
|
||||
|
||||
func parseRepoList(value string) ([]string, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return normalizeRepoIDs(strings.Split(value, ","))
|
||||
}
|
||||
|
||||
func readReposFromCSV(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 CSV 文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 CSV 文件失败: %w", err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Detect column layout: single owner/repo column, or owner+repo dual columns
|
||||
singleCol, ownerCol, repoCol := -1, -1, -1
|
||||
startRow := 0
|
||||
for i, cell := range records[0] {
|
||||
switch strings.ToLower(strings.TrimSpace(cell)) {
|
||||
case "owner/repo", "full_name":
|
||||
singleCol = i
|
||||
startRow = 1
|
||||
case "owner":
|
||||
ownerCol = i
|
||||
startRow = 1
|
||||
case "repo", "repository", "name":
|
||||
repoCol = i
|
||||
startRow = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no header — first column is owner/repo
|
||||
if singleCol == -1 && ownerCol == -1 && repoCol == -1 {
|
||||
singleCol = 0
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(records)-startRow)
|
||||
for _, record := range records[startRow:] {
|
||||
var repoID string
|
||||
if singleCol >= 0 && singleCol < len(record) {
|
||||
repoID = record[singleCol]
|
||||
} else if ownerCol >= 0 && repoCol >= 0 && ownerCol < len(record) && repoCol < len(record) {
|
||||
repoID = record[ownerCol] + "/" + record[repoCol]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
values = append(values, repoID)
|
||||
}
|
||||
return normalizeRepoIDs(values)
|
||||
}
|
||||
|
||||
func normalizeRepoIDs(values []string) ([]string, error) {
|
||||
repos := make([]string, 0, len(values))
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
repoID := strings.TrimSpace(value)
|
||||
if repoID == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(repoID, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return nil, fmt.Errorf("无效的仓库标识 %q: 请使用 owner/repo 格式", repoID)
|
||||
}
|
||||
if seen[repoID] {
|
||||
continue
|
||||
}
|
||||
seen[repoID] = true
|
||||
repos = append(repos, repoID)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
func mergeRepoLists(values ...[]string) []string {
|
||||
merged := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, repos := range values {
|
||||
for _, r := range repos {
|
||||
if seen[r] {
|
||||
continue
|
||||
}
|
||||
seen[r] = true
|
||||
merged = append(merged, r)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func parseBool(value string) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(value), "true") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import (
|
|||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
newBatchDeleteShortcut(),
|
||||
newBatchForkShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List repositories for a user or organization",
|
||||
|
|
@ -36,7 +38,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取仓库列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -49,7 +51,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "dir", Short: "d", Usage: "Target directory (default: repo name)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
rawURL, _ := ctx.RequireArg("url")
|
||||
rawURL, err := ctx.RequireArg("url")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetDir := ctx.Arg("dir")
|
||||
|
||||
var cloneURL string
|
||||
|
|
@ -65,9 +70,8 @@ func Shortcuts() []*common.Shortcut {
|
|||
} else {
|
||||
parts := strings.SplitN(rawURL, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid repository format %q: use owner/repo", rawURL)
|
||||
return fmt.Errorf("无效的仓库格式 %q: 请使用 owner/repo 格式", rawURL)
|
||||
}
|
||||
// Derive web base URL from API base URL (strip /api suffix)
|
||||
webBase := strings.TrimSuffix(strings.TrimSuffix(ctx.Client.BaseURL, "/"), "/api")
|
||||
cloneURL = fmt.Sprintf("%s/%s/%s.git", webBase, parts[0], parts[1])
|
||||
repoName = parts[1]
|
||||
|
|
@ -77,7 +81,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
targetDir = repoName
|
||||
}
|
||||
|
||||
fmt.Printf("Cloning %s into %s...\n", cloneURL, targetDir)
|
||||
fmt.Printf("正在克隆 %s 到 %s...\n", cloneURL, targetDir)
|
||||
|
||||
cmd := exec.Command("git", "clone", cloneURL, targetDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
|
|
@ -85,7 +89,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("clone failed: %w", err)
|
||||
return fmt.Errorf("克隆失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
@ -95,11 +99,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
Description: "Show repository details",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取仓库详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -117,15 +121,14 @@ func Shortcuts() []*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)
|
||||
return fmt.Errorf("获取当前用户信息失败: %w", err)
|
||||
}
|
||||
userData, _ := userEnv.Data.(map[string]interface{})
|
||||
login, _ := userData["login"].(string)
|
||||
if login == "" {
|
||||
return fmt.Errorf("cannot determine current user login")
|
||||
return fmt.Errorf("无法获取当前用户名")
|
||||
}
|
||||
userID, _ := userData["user_id"].(float64)
|
||||
body := map[string]interface{}{
|
||||
|
|
@ -141,7 +144,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s", login, name), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -151,11 +154,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
Description: "Fork a repository",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/forks", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Fork 仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -170,27 +173,25 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
|
||||
visibility := ctx.Arg("visibility")
|
||||
defaultBranch := ctx.Arg("default-branch")
|
||||
description := ctx.Arg("description")
|
||||
|
||||
// If no flags provided, show current settings
|
||||
if visibility == "" && defaultBranch == "" && description == "" {
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取仓库设置失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
// Otherwise update with provided settings
|
||||
body := map[string]interface{}{}
|
||||
if visibility != "" {
|
||||
if visibility != "public" && visibility != "private" {
|
||||
return fmt.Errorf("invalid visibility %q: must be public or private", visibility)
|
||||
return fmt.Errorf("无效的 visibility 值 %q: 必须为 public 或 private", visibility)
|
||||
}
|
||||
body["visibility"] = visibility
|
||||
}
|
||||
|
|
@ -203,7 +204,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
|
||||
env, err := ctx.CallAPI("PATCH", ctx.RepoPath(), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新仓库设置失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -213,11 +214,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
Description: "Delete a repository",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
|
|
@ -17,14 +18,17 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
keyword, _ := ctx.RequireArg("keyword")
|
||||
keyword, err := ctx.RequireArg("keyword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/projects", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("搜索仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -38,14 +42,17 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
keyword, _ := ctx.RequireArg("keyword")
|
||||
keyword, err := ctx.RequireArg("keyword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/users/list", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("搜索用户失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -61,7 +68,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
keyword, _ := ctx.RequireArg("keyword")
|
||||
keyword, err := ctx.RequireArg("keyword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -74,7 +84,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/issues", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("搜索 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
Run: func(ctx *common.RuntimeContext) error {
|
||||
env, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取当前用户信息失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -32,7 +32,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s", login), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看用户详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
Description: "List webhooks",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", v1RepoPath(ctx)+"/webhooks", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -40,7 +40,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
webhookURL, err := ctx.RequireArg("url")
|
||||
if err != nil {
|
||||
|
|
@ -68,7 +68,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/webhooks", body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -81,7 +81,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -89,7 +89,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看 Webhook 详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -108,7 +108,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -141,11 +141,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
body["active"] = active == "true"
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("at least one update field is required")
|
||||
return fmt.Errorf("至少需要指定一个要更新的字段")
|
||||
}
|
||||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/webhooks/%s", v1RepoPath(ctx), id), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -158,7 +158,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -166,7 +166,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/webhooks/%s", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -179,7 +179,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
|
|
@ -187,7 +187,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", v1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("测试 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue