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  }