package git import ( "errors" "fmt" ) // GitError 是 git 子进程返回非零或超时时的统一错误形式。Stderr 已截断 ≤2000 字符; // 上层(workspace.Service)会根据 stderr 内容映射到具体业务错误码。 type GitError struct { Args []string ExitCode int Stderr string Cause error } func (e *GitError) Error() string { return fmt.Sprintf("git %v: exit %d: %s", e.Args, e.ExitCode, e.Stderr) } func (e *GitError) Unwrap() error { return e.Cause } // IsAuthError 通过 stderr 关键字识别 401/403 类型的远端拒绝(PAT 失效、SSH key // 不被信任等)。grep 的关键字源自常见 git 远端的英文响应;模糊但可用。 func IsAuthError(err error) bool { var ge *GitError if !errors.As(err, &ge) { return false } for _, k := range []string{ "Authentication failed", "could not read Username", "Permission denied (publickey)", "403 Forbidden", "401 Unauthorized", "fatal: Authentication", } { if containsCase(ge.Stderr, k) { return true } } return false } // IsConflict 用于识别 "branch already exists" 等 worktree add 冲突场景。 func IsConflict(err error) bool { var ge *GitError if !errors.As(err, &ge) { return false } for _, k := range []string{ "already exists", "is already checked out", "already used by worktree", } { if containsCase(ge.Stderr, k) { return true } } return false } // containsCase 大小写不敏感子串匹配;避免引入 strings.EqualFold 之类逐字符开销。 func containsCase(s, sub string) bool { if len(sub) == 0 || len(s) < len(sub) { return false } S, Sub := lower(s), lower(sub) return indexOf(S, Sub) >= 0 } func lower(s string) string { b := make([]byte, len(s)) for i := 0; i < len(s); i++ { c := s[i] if c >= 'A' && c <= 'Z' { c += 'a' - 'A' } b[i] = c } return string(b) } func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }