Files
agentic-coding-workflow/internal/infra/git/errors.go
T
2026-06-10 16:06:55 +08:00

120 lines
2.8 KiB
Go

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
}