You've already forked agentic-coding-workflow
120 lines
2.8 KiB
Go
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
|
|
}
|