package git import ( "errors" "fmt" "net/url" "strings" ) // GitError 是 git 子进程返回非零或超时时的统一错误形式。Stderr 已截断 ≤2000 字符; // 上层(workspace.Service)会根据 stderr 内容映射到具体业务错误码。 type GitError struct { Args []string ExitCode int Stderr string Cause error } func (e *GitError) Error() string { // 对 Args 中形如 scheme://user:pass@host 的 URL 脱敏,避免 PAT 明文 // 写进 last_sync_error / audit。 redacted := make([]string, len(e.Args)) for i, a := range e.Args { redacted[i] = redactURLUserinfo(a) } // stderr 为空说明进程多半没跑起来(chdir 失败、二进制缺失、超时被 kill), // 此时 Cause 才是唯一有效信息,不能丢。 detail := e.Stderr if detail == "" && e.Cause != nil { detail = e.Cause.Error() } return fmt.Sprintf("git %v: exit %d: %s", redacted, e.ExitCode, detail) } // redactURLUserinfo 剥离形如 scheme://user[:pass]@host 中的 userinfo, // 输出 scheme://host...。非 URL 字符串或解析失败时原样返回。 func redactURLUserinfo(s string) string { if !strings.Contains(s, "://") { return s } u, err := url.Parse(s) if err != nil || u.User == nil { return s } u.User = nil return u.String() } 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 }