github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/git_auth.go (about) 1 package sandbox 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "strings" 8 ) 9 10 // GitAuthError 表示 git 操作因认证失败而中断。 11 type GitAuthError struct { 12 // Msg 是错误消息。 13 Msg string 14 // Err 是底层原始错误(可能是 errors.Join 聚合后的多个错误),用于 errors.Is/As 解包。 15 Err error 16 } 17 18 // Error 实现 error 接口。 19 func (e *GitAuthError) Error() string { return e.Msg } 20 21 // Unwrap 返回底层原始错误,方便调用方使用 errors.Is/As 进一步排查。 22 func (e *GitAuthError) Unwrap() error { return e.Err } 23 24 // GitUpstreamError 表示 git 操作因缺少 upstream 跟踪分支而中断。 25 type GitUpstreamError struct { 26 // Msg 是错误消息。 27 Msg string 28 // Err 是底层原始错误(可能是 errors.Join 聚合后的多个错误),用于 errors.Is/As 解包。 29 Err error 30 } 31 32 // Error 实现 error 接口。 33 func (e *GitUpstreamError) Error() string { return e.Msg } 34 35 // Unwrap 返回底层原始错误,方便调用方使用 errors.Is/As 进一步排查。 36 func (e *GitUpstreamError) Unwrap() error { return e.Err } 37 38 // InvalidArgumentError 表示传入的参数非法。 39 type InvalidArgumentError struct { 40 // Msg 是错误消息。 41 Msg string 42 } 43 44 // Error 实现 error 接口。 45 func (e *InvalidArgumentError) Error() string { return e.Msg } 46 47 // withCredentials 将 HTTPS 凭证嵌入 git 仓库 URL 中。 48 // 仅 https 协议有效;当 username 与 password 均为空时直接返回原 URL。 49 // 50 // 故意不允许 http:把 token / 密码塞进明文 URL 会让凭证暴露在非 TLS 链路上, 51 // 与对外"仅支持 HTTPS + username/password"的承诺一致。 52 func withCredentials(rawURL, username, password string) (string, error) { 53 if username == "" && password == "" { 54 return rawURL, nil 55 } 56 if username == "" || password == "" { 57 return "", &InvalidArgumentError{Msg: "Both username and password are required when using Git credentials."} 58 } 59 60 u, err := url.Parse(rawURL) 61 if err != nil { 62 return "", fmt.Errorf("parse git url: %w", err) 63 } 64 if u.Scheme != "https" { 65 return "", &InvalidArgumentError{Msg: "Only https Git URLs support username/password credentials."} 66 } 67 68 u.User = url.UserPassword(username, password) 69 return u.String(), nil 70 } 71 72 // stripCredentials 从 git URL 中移除已嵌入的凭证。 73 // 解析失败或非 http(s) URL 时原样返回。 74 func stripCredentials(rawURL string) string { 75 u, err := url.Parse(rawURL) 76 if err != nil { 77 return rawURL 78 } 79 if u.Scheme != "http" && u.Scheme != "https" { 80 return rawURL 81 } 82 if u.User == nil { 83 return rawURL 84 } 85 u.User = nil 86 return u.String() 87 } 88 89 // requireHTTPSIfHasCredentials 校验 URL 在内嵌凭证时必须使用 https。 90 // 解析失败或未内嵌凭证时返回 nil,把校验机会留给后续命令本身。 91 func requireHTTPSIfHasCredentials(rawURL string) error { 92 u, err := url.Parse(rawURL) 93 if err != nil || u.User == nil { 94 return nil 95 } 96 if u.Scheme != "https" { 97 return &InvalidArgumentError{Msg: "Only https Git URLs support inline username/password credentials."} 98 } 99 return nil 100 } 101 102 // authFailureSnippets 是常见的 git 认证失败关键字(小写匹配)。 103 // 104 // 这里只放与"凭证"语义强相关的关键字,避免把通用的 "permission denied" 105 // (工作树目录无写权限、.git/config 权限异常、锁文件权限问题等)误判为 106 // 认证失败而把调用方导向错误的排障方向。 107 var authFailureSnippets = []string{ 108 "authentication failed", 109 "terminal prompts disabled", 110 "could not read username", 111 "could not read password", 112 "invalid username or password", 113 "bad credentials", 114 "requested url returned error: 401", 115 "requested url returned error: 403", 116 "http basic: access denied", 117 } 118 119 // upstreamFailureSnippets 是常见的 git 缺失 upstream 关键字(小写匹配)。 120 var upstreamFailureSnippets = []string{ 121 "has no upstream branch", 122 "no upstream branch", 123 "no upstream configured", 124 "no tracking information for the current branch", 125 "no tracking information", 126 "set the remote as upstream", 127 "set the upstream branch", 128 "please specify which branch you want to merge with", 129 } 130 131 // gitCommandError 是内部使用的 git 命令失败错误,附带 stdout/stderr 以便后续匹配。 132 type gitCommandError struct { 133 // Cmd 是失败的 git 子命令名(如 "clone"、"push")。 134 Cmd string 135 // Result 是底层命令的执行结果,包含 ExitCode/Stdout/Stderr。 136 Result *CommandResult 137 } 138 139 // Error 实现 error 接口。 140 func (e *gitCommandError) Error() string { 141 if e.Result == nil { 142 return fmt.Sprintf("git %s failed", e.Cmd) 143 } 144 if stderr := strings.TrimSpace(e.Result.Stderr); stderr != "" { 145 return fmt.Sprintf("git %s failed (exit %d): %s", e.Cmd, e.Result.ExitCode, stderr) 146 } 147 return fmt.Sprintf("git %s failed (exit %d)", e.Cmd, e.Result.ExitCode) 148 } 149 150 // matchSnippets 在 git 命令错误的 stdout+stderr 中匹配关键字(小写匹配)。 151 func matchSnippets(err error, snippets []string) bool { 152 var ge *gitCommandError 153 if !errors.As(err, &ge) || ge.Result == nil { 154 return false 155 } 156 message := strings.ToLower(ge.Result.Stderr + "\n" + ge.Result.Stdout) 157 for _, s := range snippets { 158 if strings.Contains(message, s) { 159 return true 160 } 161 } 162 return false 163 } 164 165 // isAuthFailure 判断错误是否由 git 认证失败引起。 166 func isAuthFailure(err error) bool { return matchSnippets(err, authFailureSnippets) } 167 168 // isMissingUpstream 判断错误是否由缺失 upstream 跟踪引起。 169 func isMissingUpstream(err error) bool { return matchSnippets(err, upstreamFailureSnippets) } 170 171 // buildAuthErrorMessage 根据动作名构造 git 认证错误消息。 172 func buildAuthErrorMessage(action string, missingPassword bool) string { 173 if missingPassword { 174 return fmt.Sprintf("Git %s requires a password/token for private repositories.", action) 175 } 176 return fmt.Sprintf("Git %s requires credentials for private repositories.", action) 177 } 178 179 // buildUpstreamErrorMessage 根据动作名构造缺失 upstream 的错误消息。 180 func buildUpstreamErrorMessage(action string) string { 181 if action == "push" { 182 return "Git push failed because no upstream branch is configured. " + 183 "Set upstream once with SetUpstream=true (and optional Remote/Branch), " + 184 "or pass Remote and Branch explicitly." 185 } 186 return "Git pull failed because no upstream branch is configured. " + 187 "Pass Remote and Branch explicitly, or set upstream once (push with " + 188 "SetUpstream=true or run: git branch --set-upstream-to=origin/<branch> <branch>)." 189 }