diff --git a/cmd/api/api.go b/cmd/api/api.go index af0593d..20c09ee 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -67,10 +67,10 @@ func runAPI(c *cobra.Command, args []string) error { env, err := cli.Do(method, path, body, query) if err != nil { if apiErr, ok := err.(*client.APIError); ok { - errEnv := output.ErrorEnvelope(apiErr.Code, apiErr.Message, "") + errEnv := output.ErrorEnvelope(apiErr.Code, apiErr.Message, apiErr.Suggestion) return output.Print(errEnv, resolveFormat()) } - return err + return fmt.Errorf("API 请求失败 [%s %s]: %w", method, path, err) } return output.Print(env, resolveFormat()) diff --git a/gitlink-cli.exe b/gitlink-cli.exe deleted file mode 100644 index efff36f..0000000 Binary files a/gitlink-cli.exe and /dev/null differ diff --git a/internal/client/client.go b/internal/client/client.go index d5b2340..7db76fb 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -11,6 +11,7 @@ import ( "github.com/gitlink-org/gitlink-cli/internal/auth" "github.com/gitlink-org/gitlink-cli/internal/config" + clierrors "github.com/gitlink-org/gitlink-cli/internal/errors" "github.com/gitlink-org/gitlink-cli/internal/output" ) @@ -25,6 +26,8 @@ type APIError struct { StatusCode int Code interface{} Message string + Kind clierrors.ErrorKind + Suggestion string } func (e *APIError) Error() string { @@ -101,10 +104,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o // Check HTTP-level errors if resp.StatusCode >= 400 { + info := lookupStatusInfo(resp.StatusCode) return nil, &APIError{ StatusCode: resp.StatusCode, Code: resp.StatusCode, Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))), + Kind: info.kind, + Suggestion: info.suggestion, } } @@ -126,11 +132,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o } if statusCode != 0 && statusCode != 200 && statusCode != 1 { msg, _ := raw["message"].(string) - suggestion := suggestFix(int(statusCode)) - return output.ErrorEnvelope(int(statusCode), msg, suggestion), &APIError{ + info := lookupStatusInfo(int(statusCode)) + return output.ErrorEnvelope(int(statusCode), msg, info.suggestion), &APIError{ StatusCode: int(statusCode), Code: int(statusCode), Message: msg, + Kind: info.kind, + Suggestion: info.suggestion, } } } @@ -177,17 +185,43 @@ func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error) return c.Do("DELETE", path, nil, query) } -func suggestFix(code int) string { - switch code { - case 401: - return "请先运行 gitlink-cli auth login 登录" - case 403: - return "权限不足,请确认账户权限或联系项目管理员" - case 404: - return "资源不存在,请检查 owner/repo/id 是否正确" - case 422: - return "参数校验失败,请检查请求参数" - default: - return "" +type statusInfo struct { + kind clierrors.ErrorKind + message string + suggestion string +} + +var statusMessages = map[int]statusInfo{ + -2: {clierrors.KindAuth, "未登录或 Token 已过期", + "运行 gitlink-cli auth login 重新登录,或检查 GITLINK_TOKEN 环境变量"}, + -1: {clierrors.KindInput, "参数校验失败", + "检查必填参数是否缺失、参数格式是否正确,运行 gitlink-cli <命令> --help 查看用法"}, + 0: {clierrors.KindUnknown, "操作失败", ""}, + // Standard HTTP codes + 401: {clierrors.KindAuth, "认证失败", + "运行 gitlink-cli auth login 登录,或检查 GITLINK_TOKEN 环境变量"}, + 403: {clierrors.KindForbidden, "权限不足", + "请确认账号有此仓库的访问权限,或联系项目管理员"}, + 404: {clierrors.KindNotFound, "资源不存在", + "检查 owner/repo/id 是否正确,资源可能已被删除"}, + 422: {clierrors.KindInput, "参数校验失败", + "检查请求参数格式,运行 gitlink-cli <命令> --help 查看用法"}, + 429: {clierrors.KindServer, "请求过于频繁", + "稍等片刻后重试"}, + 500: {clierrors.KindServer, "服务器内部错误", + "稍等后重试,如持续出现请联系平台管理员"}, + 502: {clierrors.KindServer, "网关错误", + "服务器暂时不可用,稍等后重试"}, + 503: {clierrors.KindServer, "服务暂时不可用", + "服务器正在维护,稍等后重试"}, +} + +func lookupStatusInfo(code int) statusInfo { + if info, ok := statusMessages[code]; ok { + return info + } + return statusInfo{ + kind: clierrors.KindUnknown, + message: fmt.Sprintf("API 返回错误码 %d", code), } } diff --git a/internal/context/repo.go b/internal/context/repo.go index 0c9adfb..03ab176 100644 --- a/internal/context/repo.go +++ b/internal/context/repo.go @@ -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("无法自动检测 owner/repo: %w\n 请使用 --owner 和 --repo 参数显式指定,或切换到 git 仓库目录下执行", err) } } diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..d239c9d --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,115 @@ +package errors + +import ( + "fmt" + "strings" +) + +// ErrorKind categorizes errors by user-actionability. +type ErrorKind string + +const ( + KindAuth ErrorKind = "auth" // Login/token issues — user can re-login + KindInput ErrorKind = "input" // Parameter issues — user can fix arguments + KindConfig ErrorKind = "config" // Config file issues — user can edit config + KindNetwork ErrorKind = "network" // Network issues — user can check/retry + KindGit ErrorKind = "git" // Git repo issues — user needs correct directory + KindServer ErrorKind = "server" // Server-side error — user should wait or contact admin + KindNotFound ErrorKind = "not_found" // Resource not found — user can check ID + KindForbidden ErrorKind = "forbidden" // Permission denied — user can request access + KindUnknown ErrorKind = "unknown" // Unclassified error +) + +// CLIError is the unified CLI error type with multi-layered information. +type CLIError struct { + Kind ErrorKind // Error category for programmatic handling + Message string // Human-readable description of what went wrong + Detail string // Low-level technical detail (shown in debug mode) + Suggestion string // Actionable advice for the user + Command string // The command that triggered the error (e.g., "issue +create") + Cause error // The underlying error +} + +func (e *CLIError) Error() string { + var b strings.Builder + + // Header line: kind + command + b.WriteString(string(e.Kind)) + b.WriteString(" error") + if e.Command != "" { + b.WriteString(" — ") + b.WriteString(e.Command) + } + + // Body: message + if e.Message != "" { + b.WriteString("\n\n reason: ") + b.WriteString(e.Message) + } + + // Suggestion + if e.Suggestion != "" { + b.WriteString("\n suggestion: ") + b.WriteString(e.Suggestion) + } + + // Detail (always included in Error() so users see the raw cause) + if e.Detail != "" { + b.WriteString("\n detail: ") + b.WriteString(e.Detail) + } + + return b.String() +} + +func (e *CLIError) Unwrap() error { + return e.Cause +} + +// New creates a CLIError with the given parameters. +func New(kind ErrorKind, message, suggestion string) *CLIError { + return &CLIError{ + Kind: kind, + Message: message, + Suggestion: suggestion, + } +} + +// Wrap creates a CLIError that wraps an underlying cause. +func Wrap(kind ErrorKind, message, suggestion string, cause error) *CLIError { + return &CLIError{ + Kind: kind, + Message: message, + Suggestion: suggestion, + Cause: cause, + Detail: cause.Error(), + } +} + +// WithCommand sets the command context on the error. +func (e *CLIError) WithCommand(cmd string) *CLIError { + e.Command = cmd + return e +} + +// InputError is a convenience constructor for parameter errors. +func InputError(message, suggestion string) *CLIError { + return New(KindInput, message, suggestion) +} + +// AuthError is a convenience constructor for authentication errors. +func AuthError(message, suggestion string) *CLIError { + return New(KindAuth, message, suggestion) +} + +// ConfigError creates a config-related error with the config file path in the suggestion. +func ConfigError(message string, cause error) *CLIError { + return Wrap(KindConfig, message, + fmt.Sprintf("检查配置文件 %s 是否正确", configPathPlaceholder()), cause) +} + +// configPathPlaceholder avoids circular import; the actual path will be resolved +// in output formatting. +func configPathPlaceholder() string { + return "~/.config/gitlink-cli/config.yaml" +} diff --git a/shortcuts/branch/branch.go b/shortcuts/branch/branch.go index 37684b6..0a5d1ff 100644 --- a/shortcuts/branch/branch.go +++ b/shortcuts/branch/branch.go @@ -25,7 +25,7 @@ func Shortcuts() []*common.Shortcut { 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) }, @@ -41,7 +41,10 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, _ := ctx.RequireArg("name") + name, err := ctx.RequireArg("name", "--name feature/new-thing") + if err != nil { + return err + } 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) }, @@ -67,13 +70,16 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, _ := ctx.RequireArg("name") + name, err := ctx.RequireArg("name", "--name feature/new-thing") + if err != nil { + return err + } 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) }, @@ -88,13 +94,16 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, _ := ctx.RequireArg("name") + name, err := ctx.RequireArg("name", "--name feature/new-thing") + if err != nil { + return err + } 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) }, @@ -109,11 +118,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--name feature/new-thing") 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) }, }, diff --git a/shortcuts/ci/ci.go b/shortcuts/ci/ci.go index eb15ad3..9ce406c 100644 --- a/shortcuts/ci/ci.go +++ b/shortcuts/ci/ci.go @@ -25,7 +25,7 @@ func Shortcuts() []*common.Shortcut { q.Set("limit", ctx.Arg("limit")) env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/builds", q) if err != nil { - return err + return fmt.Errorf("获取 CI 构建列表失败: %w", err) } return ctx.Output(env) }, @@ -42,7 +42,10 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - build, _ := ctx.RequireArg("build") + build, err := ctx.RequireArg("build", "--build 42") + if err != nil { + return err + } 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) }, @@ -68,11 +71,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--build 42") 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) }, }, @@ -86,11 +92,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--build 42") 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) }, }, diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 056b595..00df958 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -26,7 +26,8 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) { } } - ctx, err := NewRuntimeContext(flagValues) + commandName := parent.Use + " +" + s.Name + ctx, err := NewRuntimeContext(flagValues, commandName) if err != nil { return err } @@ -36,20 +37,21 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) { } for _, f := range s.Flags { + usage := f.Usage + if f.Required { + usage = usage + " [required]" + } if f.Bool { defaultValue, _ := strconv.ParseBool(f.Default) if f.Short != "" { - cmd.Flags().BoolP(f.Name, f.Short, defaultValue, f.Usage) + cmd.Flags().BoolP(f.Name, f.Short, defaultValue, usage) } else { - cmd.Flags().Bool(f.Name, defaultValue, f.Usage) + cmd.Flags().Bool(f.Name, defaultValue, usage) } } else if f.Short != "" { - cmd.Flags().StringP(f.Name, f.Short, f.Default, f.Usage) + cmd.Flags().StringP(f.Name, f.Short, f.Default, usage) } else { - cmd.Flags().String(f.Name, f.Default, f.Usage) - } - if f.Required { - cmd.MarkFlagRequired(f.Name) + cmd.Flags().String(f.Name, f.Default, usage) } } diff --git a/shortcuts/common/types.go b/shortcuts/common/types.go index 15441c9..3a4ac46 100644 --- a/shortcuts/common/types.go +++ b/shortcuts/common/types.go @@ -8,6 +8,7 @@ import ( "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" "github.com/gitlink-org/gitlink-cli/internal/client" "github.com/gitlink-org/gitlink-cli/internal/context" + clierrors "github.com/gitlink-org/gitlink-cli/internal/errors" "github.com/gitlink-org/gitlink-cli/internal/output" ) @@ -31,15 +32,16 @@ type Flag struct { // RuntimeContext provides helpers for shortcut implementations. type RuntimeContext struct { - Client *client.Client - Owner string - Repo string - Format string - Args map[string]string + Client *client.Client + Owner string + Repo string + Format string + CommandName string + Args map[string]string } // NewRuntimeContext creates a RuntimeContext with auto-resolved owner/repo. -func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) { +func NewRuntimeContext(args map[string]string, commandName string) (*RuntimeContext, error) { cli, err := client.New() if err != nil { return nil, err @@ -52,11 +54,12 @@ func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) { } return &RuntimeContext{ - Client: cli, - Owner: cmdutil.Owner, - Repo: cmdutil.Repo, - Format: format, - Args: args, + Client: cli, + Owner: cmdutil.Owner, + Repo: cmdutil.Repo, + Format: format, + CommandName: commandName, + Args: args, }, nil } @@ -109,11 +112,18 @@ func (ctx *RuntimeContext) Arg(name string) string { return "" } -// RequireArg returns a flag value or an error if not set. -func (ctx *RuntimeContext) RequireArg(name string) (string, error) { +// RequireArg returns a flag value or a CLIError if not set. +func (ctx *RuntimeContext) RequireArg(name, example string) (string, error) { v := ctx.Arg(name) if v == "" { - return "", fmt.Errorf("required flag --%s is missing", name) + suggestion := fmt.Sprintf("请提供 --%s 参数", name) + if example != "" { + suggestion += fmt.Sprintf(",例如:%s", example) + } + return "", clierrors.InputError( + fmt.Sprintf("required flag --%s is missing", name), + suggestion, + ).WithCommand(ctx.CommandName) } return v, nil } diff --git a/shortcuts/issue/batch.go b/shortcuts/issue/batch.go index ea4e442..429a12a 100644 --- a/shortcuts/issue/batch.go +++ b/shortcuts/issue/batch.go @@ -78,6 +78,15 @@ var tagIDs = map[string]int{ "搁置": 315532, } +// labelNames returns all known tag names from the given mapping. +func labelNames(tags map[string]int) string { + var names []string + for name := range tags { + names = append(names, name) + } + return strings.Join(names, ", ") +} + // BatchResult is a single item result in a batch operation. type BatchResult struct { Number string `json:"number" yaml:"number"` @@ -566,15 +575,16 @@ func parseTracker(label string) (int, error) { } } -// parseLabel converts a label name to its GitLink tag ID -func parseLabel(name string) (int, error) { - if id, ok := tagIDs[name]; ok && id != 0 { +// parseLabel converts a label name to its GitLink tag ID. +// tags is the project's name→id mapping from resolveIssueTags. +func parseLabel(name string, tags map[string]int) (int, error) { + if id, ok := tags[name]; ok && id != 0 { return id, nil } if id, err := strconv.Atoi(name); err == nil { return id, nil } - return 0, fmt.Errorf("invalid label %q: not found in tagIDs mapping", name) + return 0, fmt.Errorf("invalid label %q: not found in project issue tags", name) } func collectIssueNumbers(numbersValue, csvPath string) ([]string, error) { diff --git a/shortcuts/issue/batch_create.go b/shortcuts/issue/batch_create.go index aecd997..0c59c1d 100644 --- a/shortcuts/issue/batch_create.go +++ b/shortcuts/issue/batch_create.go @@ -3,12 +3,76 @@ package issue import ( "encoding/csv" "fmt" + "net/url" "os" + "strconv" "strings" + "sync" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) +var issueTagCache sync.Map + +// resolveIssueTags fetches the project's issue tags and returns a name→id mapping. +// Results are cached per owner/repo. +func resolveIssueTags(ctx *common.RuntimeContext) (map[string]int, error) { + key := ctx.Owner + "/" + ctx.Repo + if cached, ok := issueTagCache.Load(key); ok { + return cached.(map[string]int), nil + } + + path := fmt.Sprintf("/v1/%s/%s/issue_tags", ctx.Owner, ctx.Repo) + q := url.Values{} + q.Set("only_name", "true") + env, err := ctx.CallAPIWithQuery("GET", path, q) + if err != nil { + return nil, fmt.Errorf("获取项目标签列表失败: %w", err) + } + + data, ok := env.Data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("标签列表响应格式异常") + } + + rawTags, ok := data["issue_tags"].([]interface{}) + if !ok { + return nil, fmt.Errorf("标签列表响应缺少 issue_tags 字段") + } + + tags := make(map[string]int, len(rawTags)) + for _, item := range rawTags { + tag, ok := item.(map[string]interface{}) + if !ok { + continue + } + name, _ := tag["name"].(string) + if name == "" { + continue + } + var id int + switch v := tag["id"].(type) { + case float64: + id = int(v) + case int: + id = v + default: + id, _ = strconv.Atoi(fmt.Sprintf("%v", v)) + } + if id == 0 { + continue + } + tags[name] = id + } + + if len(tags) == 0 { + return nil, fmt.Errorf("项目没有配置任何标签,请先在 GitLink 网页端创建标签") + } + + issueTagCache.Store(key, tags) + return tags, nil +} + func newBatchCreateShortcut() *common.Shortcut { return &common.Shortcut{ Name: "batch-create", @@ -49,6 +113,11 @@ func runBatchCreate(ctx *common.RuntimeContext) error { return err } + tags, err := resolveIssueTags(ctx) + if err != nil { + return err + } + dryRun := parseBool(ctx.Arg("dry-run")) template := strings.ToLower(strings.TrimSpace(ctx.Arg("template"))) @@ -99,7 +168,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error { continue } - body := buildCreateBody(ctx, input, template) + body := buildCreateBody(ctx, input, template, tags) env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body) if err != nil { result.Status = "failed" @@ -126,7 +195,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error { return nil } -func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, template string) map[string]interface{} { +func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, template string, tags map[string]int) map[string]interface{} { statusID := statusNew if input.Status != "" { if sid, err := parseStatus(input.Status); err == nil { @@ -143,9 +212,9 @@ func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, templat if template != "" { body["description"] = buildTemplateDescription(input, template) if template == "bug" { - body["issue_tag_ids"] = []interface{}{tagIDs["缺陷"]} + body["issue_tag_ids"] = []interface{}{tags["缺陷"]} } else if template == "feature" { - body["issue_tag_ids"] = []interface{}{tagIDs["功能"]} + body["issue_tag_ids"] = []interface{}{tags["功能"]} } } else if input.Body != "" { body["description"] = input.Body @@ -157,7 +226,7 @@ func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, templat } } if input.Label != "" { - if tid, err := parseLabel(input.Label); err == nil { + if tid, err := parseLabel(input.Label, tags); err == nil { body["issue_tag_ids"] = []interface{}{tid} } } diff --git a/shortcuts/issue/batch_create_test.go b/shortcuts/issue/batch_create_test.go index 9425361..b91680e 100644 --- a/shortcuts/issue/batch_create_test.go +++ b/shortcuts/issue/batch_create_test.go @@ -12,6 +12,15 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) +// testTags is a static name→id mapping used by unit tests. +var testTags = map[string]int{ + "缺陷": 315526, + "功能": 315527, + "文档": 315533, + "任务": 315530, + "测试": 315534, +} + // ---- helpers ---- func findShortcut(t *testing.T, name string) *common.Shortcut { @@ -25,6 +34,21 @@ func findShortcut(t *testing.T, name string) *common.Shortcut { return nil } +// mockTagsHandler returns a handler that responds to the issue_tags API. +func mockTagsHandler(t *testing.T) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { + tags := make([]map[string]interface{}, 0, len(testTags)) + for name, id := range testTags { + tags = append(tags, map[string]interface{}{ + "id": float64(id), + "name": name, + }) + } + writeJSONResp(t, w, map[string]interface{}{"issue_tags": tags}) + } +} + func runBatchCreateShortcut(t *testing.T, server *httptest.Server, args map[string]string) error { t.Helper() s := findShortcut(t, "batch-create") @@ -57,6 +81,10 @@ func decodeReqBody(t *testing.T, r *http.Request) map[string]interface{} { func TestBatchCreate_DryRun(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") { + mockTagsHandler(t)(w, r) + return + } t.Fatalf("no API calls expected in dry-run mode, got %s %s", r.Method, r.URL.Path) })) defer server.Close() @@ -75,6 +103,8 @@ func TestBatchCreate_FromTitles(t *testing.T) { callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"): + mockTagsHandler(t)(w, r) case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"): callCount++ body := decodeReqBody(t, r) @@ -114,6 +144,10 @@ func TestBatchCreate_FromTitles(t *testing.T) { func TestBatchCreate_NoTitles(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") { + mockTagsHandler(t)(w, r) + return + } t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) })) defer server.Close() @@ -131,12 +165,15 @@ func TestBatchCreate_FromCSV(t *testing.T) { var created []map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues") { + switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"): + mockTagsHandler(t)(w, r) + case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"): created = append(created, decodeReqBody(t, r)) writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(1)}) - return + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) })) defer server.Close() @@ -161,6 +198,10 @@ func TestBatchCreate_CSVMissingTitleColumn(t *testing.T) { csvPath := writeTempCSV(t, "name,description\nval1,desc1\n") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") { + mockTagsHandler(t)(w, r) + return + } t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) })) defer server.Close() @@ -175,6 +216,10 @@ func TestBatchCreate_CSVOnlyHeader(t *testing.T) { csvPath := writeTempCSV(t, "title,description\n") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") { + mockTagsHandler(t)(w, r) + return + } t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) })) defer server.Close() @@ -188,12 +233,19 @@ func TestBatchCreate_CSVOnlyHeader(t *testing.T) { func TestBatchCreate_PartialFailure(t *testing.T) { callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount++ - if callCount == 2 { - w.WriteHeader(http.StatusUnprocessableEntity) - return + switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"): + mockTagsHandler(t)(w, r) + case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"): + callCount++ + if callCount == 2 { + w.WriteHeader(http.StatusUnprocessableEntity) + return + } + writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(callCount)}) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } - writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(callCount)}) })) defer server.Close() @@ -223,7 +275,7 @@ func intVal(v interface{}) int { func TestBuildCreateBody_Basic(t *testing.T) { ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}} input := createIssueInput{Title: "Test issue", Status: "new"} - body := buildCreateBody(ctx, input, "") + body := buildCreateBody(ctx, input, "", testTags) if body["subject"] != "Test issue" { t.Fatalf("subject: got %v", body["subject"]) } @@ -248,7 +300,7 @@ func TestBuildCreateBody_BugTemplate(t *testing.T) { Expected: "正常登录", Actual: "报错 500", } - body := buildCreateBody(ctx, input, "bug") + body := buildCreateBody(ctx, input, "bug", testTags) if body["subject"] != "登录报错" { t.Fatalf("subject: got %v", body["subject"]) } @@ -266,8 +318,8 @@ func TestBuildCreateBody_BugTemplate(t *testing.T) { t.Fatal("bug template missing issue_tag_ids") } else { ids := rawTags.([]interface{}) - if intVal(ids[0]) != tagIDs["缺陷"] { - t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], tagIDs["缺陷"]) + if intVal(ids[0]) != testTags["缺陷"] { + t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"]) } } } @@ -279,7 +331,7 @@ func TestBuildCreateBody_FeatureTemplate(t *testing.T) { UserStory: "作为用户,我想搜索内容", Acceptance: "搜索结果正确显示", } - body := buildCreateBody(ctx, input, "feature") + body := buildCreateBody(ctx, input, "feature", testTags) desc, _ := body["description"].(string) if !strings.Contains(desc, "## 用户故事") { t.Fatal("feature description missing user story header") @@ -299,14 +351,14 @@ func TestBuildCreateBody_WithPriorityLabel(t *testing.T) { Priority: "high", Label: "缺陷", } - body := buildCreateBody(ctx, input, "") + body := buildCreateBody(ctx, input, "", testTags) if intVal(body["priority_id"]) != 3 { t.Fatalf("priority_id: got %v (type %T), want 3 (high)", body["priority_id"], body["priority_id"]) } if rawTags, ok := body["issue_tag_ids"]; ok { ids := rawTags.([]interface{}) - if intVal(ids[0]) != tagIDs["缺陷"] { - t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], tagIDs["缺陷"]) + if intVal(ids[0]) != testTags["缺陷"] { + t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"]) } } else { t.Fatal("missing issue_tag_ids") @@ -557,23 +609,23 @@ func TestParsePriorityInvalid(t *testing.T) { } func TestParseLabelValid(t *testing.T) { - id, err := parseLabel("缺陷") + id, err := parseLabel("缺陷", testTags) if err != nil { t.Fatalf("unexpected error: %v", err) } - if id == 0 { - t.Fatal("expected non-zero tag ID") + if id != testTags["缺陷"] { + t.Fatalf("got %d, want %d", id, testTags["缺陷"]) } } func TestParseLabelInvalid(t *testing.T) { - if _, err := parseLabel("不存在的标签"); err == nil { + if _, err := parseLabel("不存在的标签", testTags); err == nil { t.Fatal("expected error") } } func TestParseLabelNumeric(t *testing.T) { - id, err := parseLabel("999") + id, err := parseLabel("999", testTags) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -583,7 +635,7 @@ func TestParseLabelNumeric(t *testing.T) { } func TestLabelNamesReturnsAll(t *testing.T) { - names := labelNames() + names := labelNames(testTags) if !strings.Contains(names, "缺陷") { t.Fatal("missing 缺陷 in label names") } @@ -597,7 +649,7 @@ func TestLabelNamesReturnsAll(t *testing.T) { func TestBuildCreateBody_DefaultStatus(t *testing.T) { ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}} input := createIssueInput{Title: "t", Status: ""} - body := buildCreateBody(ctx, input, "") + body := buildCreateBody(ctx, input, "", testTags) if intVal(body["status_id"]) != 1 { t.Fatalf("default status_id: got %v (type %T), want 1", body["status_id"], body["status_id"]) } @@ -606,7 +658,7 @@ func TestBuildCreateBody_DefaultStatus(t *testing.T) { func TestBuildCreateBody_ClosedStatus(t *testing.T) { ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}} input := createIssueInput{Title: "t", Status: "closed"} - body := buildCreateBody(ctx, input, "") + body := buildCreateBody(ctx, input, "", testTags) if intVal(body["status_id"]) != 5 { t.Fatalf("closed status_id: got %v (type %T), want 5", body["status_id"], body["status_id"]) } diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index aa77ff5..a716e5b 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -47,7 +47,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) }, @@ -66,7 +66,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, err := ctx.RequireArg("title") + title, err := ctx.RequireArg("title", `--title "Bug: 登录页崩溃"`) if err != nil { return err } @@ -87,7 +87,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) }, @@ -102,13 +102,13 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - number, err := ctx.RequireArg("number") + number, err := ctx.RequireArg("number", "--number 42") if err != nil { return err } 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) }, @@ -123,7 +123,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - number, err := ctx.RequireArg("number") + number, err := ctx.RequireArg("number", "--number 42") if err != nil { return err } @@ -139,7 +139,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) }, @@ -157,7 +157,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - number, err := ctx.RequireArg("number") + number, err := ctx.RequireArg("number", "--number 42") if err != nil { return err } @@ -192,7 +192,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) }, @@ -208,11 +208,11 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - number, err := ctx.RequireArg("number") + number, err := ctx.RequireArg("number", "--number 42") if err != nil { return err } - body, err := ctx.RequireArg("body") + body, err := ctx.RequireArg("body", `--body "可以这样复现..."`) if err != nil { return err } @@ -221,7 +221,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("添加 Issue 评论失败: %w", err) } return ctx.Output(env) }, @@ -232,7 +232,7 @@ 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 { diff --git a/shortcuts/org/org.go b/shortcuts/org/org.go index 4cd6299..b1aa202 100644 --- a/shortcuts/org/org.go +++ b/shortcuts/org/org.go @@ -22,7 +22,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 +34,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", "--id my-org") 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 +54,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", "--id my-org") + 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 +76,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", `--name "My Organization"`) + if err != nil { + return err + } payload := map[string]interface{}{ "name": name, } @@ -79,7 +88,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) }, diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 5f4a713..80c5f9d 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -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) }, @@ -48,8 +48,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, _ := ctx.RequireArg("title") - head, _ := ctx.RequireArg("head") + title, err := ctx.RequireArg("title", `--title "Fix login crash"`) + if err != nil { + return err + } + head, err := ctx.RequireArg("head", `--head feat/new-login`) + if err != nil { + return err + } 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) }, @@ -79,11 +85,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - id, _ := ctx.RequireArg("id") - env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil) + id, err := ctx.RequireArg("id", "--id 42") 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) }, }, @@ -98,7 +107,10 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - id, _ := ctx.RequireArg("id") + id, err := ctx.RequireArg("id", "--id 42") + if err != nil { + return err + } 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) }, @@ -123,11 +135,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--id 42") 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) }, }, @@ -141,11 +156,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--id 42") 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) }, }, @@ -159,11 +177,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return 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", "--id 42") 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) }, }, @@ -178,12 +199,18 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - id, _ := ctx.RequireArg("id") - body, _ := ctx.RequireArg("body") + id, err := ctx.RequireArg("id", "--id 42") + if err != nil { + return err + } + body, err := ctx.RequireArg("body", `--body "Looks good to me"`) + if err != nil { + return err + } 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 { @@ -195,7 +222,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) }, diff --git a/shortcuts/release/release.go b/shortcuts/release/release.go index 06240bc..58f200a 100644 --- a/shortcuts/release/release.go +++ b/shortcuts/release/release.go @@ -26,7 +26,7 @@ func Shortcuts() []*common.Shortcut { 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) }, @@ -45,8 +45,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - tag, _ := ctx.RequireArg("tag") - name, _ := ctx.RequireArg("name") + tag, err := ctx.RequireArg("tag", "--tag v1.0.0") + if err != nil { + return err + } + name, err := ctx.RequireArg("name", `--name "Version 1.0.0"`) + if err != nil { + return err + } payload := map[string]interface{}{ "tag_name": tag, "name": name, @@ -62,7 +68,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) }, @@ -77,11 +83,14 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - id, _ := ctx.RequireArg("id") - env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil) + id, err := ctx.RequireArg("id", "--id 1") 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) }, }, @@ -95,7 +104,10 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - id, _ := ctx.RequireArg("id") + id, err := ctx.RequireArg("id", "--id 1") + if err != nil { + return err + } _, 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. @@ -108,7 +120,7 @@ func Shortcuts() []*common.Shortcut { }, nil)) } // Release still exists — delete truly failed - return delErr + return fmt.Errorf("删除 Release 失败: %w", delErr) } return ctx.Output(output.SuccessEnvelope(map[string]interface{}{ "message": "删除成功", diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 1fa9d29..67d02ce 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -35,7 +35,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 +49,7 @@ func Shortcuts() []*common.Shortcut { } env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil) if err != nil { - return err + return fmt.Errorf("查看仓库失败: %w", err) } return ctx.Output(env) }, @@ -63,14 +63,14 @@ func Shortcuts() []*common.Shortcut { {Name: "private", Usage: "Make repository private (true/false)", Default: "false"}, }, Run: func(ctx *common.RuntimeContext) error { - name, err := ctx.RequireArg("name") + name, err := ctx.RequireArg("name", `--name "my-project"`) 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) @@ -91,7 +91,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) }, @@ -105,7 +105,7 @@ func Shortcuts() []*common.Shortcut { } env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/forks", nil) if err != nil { - return err + return fmt.Errorf("Fork 仓库失败: %w", err) } return ctx.Output(env) }, @@ -119,7 +119,7 @@ func Shortcuts() []*common.Shortcut { } env, err := ctx.CallAPI("DELETE", ctx.RepoPath(), nil) if err != nil { - return err + return fmt.Errorf("删除仓库失败: %w", err) } return ctx.Output(env) }, diff --git a/shortcuts/search/search.go b/shortcuts/search/search.go index 443dec3..ae1baaa 100644 --- a/shortcuts/search/search.go +++ b/shortcuts/search/search.go @@ -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", "--keyword my-project") + 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", "--keyword zhangsan") + 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) }, diff --git a/shortcuts/user/user.go b/shortcuts/user/user.go index cfcaa2a..a59eae4 100644 --- a/shortcuts/user/user.go +++ b/shortcuts/user/user.go @@ -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) }, @@ -26,13 +26,13 @@ func Shortcuts() []*common.Shortcut { {Name: "login", Short: "l", Usage: "User login name", Required: true}, }, Run: func(ctx *common.RuntimeContext) error { - login, err := ctx.RequireArg("login") + login, err := ctx.RequireArg("login", "--login zhangsan") if err != nil { return err } 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) }, diff --git a/shortcuts/webhook/webhook.go b/shortcuts/webhook/webhook.go index ebeb5e2..10fcbe5 100644 --- a/shortcuts/webhook/webhook.go +++ b/shortcuts/webhook/webhook.go @@ -72,7 +72,7 @@ func Shortcuts() []*common.Shortcut { q.Set("limit", ctx.Arg("limit")) env, err := ctx.CallAPIWithQuery("GET", webhookRepoPath(ctx)+"/webhooks", q) if err != nil { - return err + return fmt.Errorf("获取 Webhook 列表失败: %w", err) } return ctx.Output(env) }, @@ -92,7 +92,7 @@ func Shortcuts() []*common.Shortcut { return err } - webhookURL, err := ctx.RequireArg("url") + webhookURL, err := ctx.RequireArg("url", "--url https://example.com/hook") if err != nil { return err } @@ -125,7 +125,7 @@ func Shortcuts() []*common.Shortcut { env, err := ctx.CallAPI("POST", webhookRepoPath(ctx)+"/webhooks", payload) if err != nil { - return err + return fmt.Errorf("创建 Webhook 失败: %w", err) } return ctx.Output(env) }, @@ -147,7 +147,7 @@ func Shortcuts() []*common.Shortcut { return err } - webhookID, err := ctx.RequireArg("id") + webhookID, err := ctx.RequireArg("id", "--id 1") if err != nil { return err } @@ -163,7 +163,7 @@ func Shortcuts() []*common.Shortcut { if webhookURL == "" { getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil) if err != nil { - return fmt.Errorf("failed to get current webhook info: %w", err) + return fmt.Errorf("获取 Webhook 当前信息失败: %w", err) } webhookData, ok := getEnv.Data.(map[string]interface{}) if !ok { @@ -201,7 +201,7 @@ func Shortcuts() []*common.Shortcut { env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), payload) if err != nil { - return err + return fmt.Errorf("更新 Webhook 失败: %w", err) } return ctx.Output(env) }, @@ -217,7 +217,7 @@ func Shortcuts() []*common.Shortcut { return err } - webhookID, err := ctx.RequireArg("id") + webhookID, err := ctx.RequireArg("id", "--id 1") if err != nil { return err } @@ -232,7 +232,7 @@ func Shortcuts() []*common.Shortcut { "message": "Webhook deleted successfully", }, nil)) } - return delErr + return fmt.Errorf("删除 Webhook 失败: %w", delErr) } return ctx.Output(output.SuccessEnvelope(map[string]interface{}{ "message": "Webhook deleted successfully", @@ -251,7 +251,7 @@ func Shortcuts() []*common.Shortcut { return err } - webhookID, err := ctx.RequireArg("id") + webhookID, err := ctx.RequireArg("id", "--id 1") if err != nil { return err } @@ -263,7 +263,7 @@ func Shortcuts() []*common.Shortcut { env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", webhookRepoPath(ctx), webhookID), nil) if err != nil { - return err + return fmt.Errorf("测试 Webhook 失败: %w", err) } return ctx.Output(env) }, @@ -279,14 +279,14 @@ func Shortcuts() []*common.Shortcut { return err } - webhookID, err := ctx.RequireArg("id") + webhookID, err := ctx.RequireArg("id", "--id 1") if err != nil { return err } env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil) if err != nil { - return err + return fmt.Errorf("查看 Webhook 详情失败: %w", err) } return ctx.Output(env) }, diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index 914f57f..2221866 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -140,7 +140,7 @@ func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName string) (s q.Set("pageName", pageName) env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q) if err != nil { - return "", err + return "", fmt.Errorf("获取 Wiki 页面现有内容失败: %w", err) } data, ok := env.Data.(map[string]interface{}) if !ok { @@ -223,7 +223,7 @@ func Shortcuts() []*common.Shortcut { q.Set("projectId", projectID) env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("wikiPages"), q) if err != nil { - return err + return fmt.Errorf("获取 Wiki 页面列表失败: %w", err) } cleanWikiList(env) return ctx.Output(env) @@ -239,7 +239,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, err := ctx.RequireArg("title") + title, err := ctx.RequireArg("title", `--title "Home Page"`) if err != nil { return err } @@ -254,7 +254,7 @@ func Shortcuts() []*common.Shortcut { q.Set("pageName", title) env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q) if err != nil { - return err + return fmt.Errorf("查看 Wiki 页面失败: %w", err) } return outputWithDecodedContent(ctx, env) }, @@ -272,7 +272,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, err := ctx.RequireArg("title") + title, err := ctx.RequireArg("title", `--title "Home Page"`) if err != nil { return err } @@ -300,7 +300,7 @@ func Shortcuts() []*common.Shortcut { env, err := callWikiAPI(ctx, "POST", wikiPath("createWiki"), body) if err != nil { - return err + return fmt.Errorf("创建 Wiki 页面失败: %w", err) } return ctx.Output(env) }, @@ -320,7 +320,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, err := ctx.RequireArg("title") + title, err := ctx.RequireArg("title", `--title "Home Page"`) if err != nil { return err } @@ -373,7 +373,7 @@ func Shortcuts() []*common.Shortcut { env, err := callWikiAPI(ctx, "PUT", wikiPath("updateWiki"), body) if err != nil { - return err + return fmt.Errorf("更新 Wiki 页面失败: %w", err) } return ctx.Output(env) }, @@ -388,7 +388,7 @@ func Shortcuts() []*common.Shortcut { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - title, err := ctx.RequireArg("title") + title, err := ctx.RequireArg("title", `--title "Home Page"`) if err != nil { return err } @@ -418,7 +418,7 @@ func Shortcuts() []*common.Shortcut { "message": "Wiki page deleted successfully", }) } - return delErr + return fmt.Errorf("删除 Wiki 页面失败: %w", delErr) } return ctx.OutputData(map[string]string{ "message": "Wiki page deleted successfully",