fix:统一错误提示优化+Issue标签ID动态获取

This commit is contained in:
camelliamc 2026-06-04 10:36:41 +08:00
parent 297a2ccbbe
commit 89afa968fd
21 changed files with 538 additions and 170 deletions

View File

@ -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())

Binary file not shown.

View File

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

View File

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

115
internal/errors/errors.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"])
}

View File

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

View File

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

View File

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

View File

@ -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": "删除成功",

View File

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

View File

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

View File

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

View File

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

View File

@ -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",