github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/git_args.go (about)

     1  package sandbox
     2  
     3  import (
     4  	"strconv"
     5  	"strings"
     6  )
     7  
     8  // shellEscape 对字符串做 shell 单引号转义,使其可安全嵌入命令字符串。
     9  func shellEscape(value string) string {
    10  	return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
    11  }
    12  
    13  // buildGitCommand 拼装一条 shell 安全的 git 命令字符串。
    14  // repoPath 非空时附加 -C <repoPath>。
    15  func buildGitCommand(args []string, repoPath string) string {
    16  	parts := make([]string, 0, len(args)+3)
    17  	parts = append(parts, "git")
    18  	if repoPath != "" {
    19  		parts = append(parts, "-C", repoPath)
    20  	}
    21  	parts = append(parts, args...)
    22  	for i, p := range parts {
    23  		parts[i] = shellEscape(p)
    24  	}
    25  	return strings.Join(parts, " ")
    26  }
    27  
    28  // buildPushArgs 构造 git push 命令的参数列表。
    29  // target 为最终使用的 remote 名(已合并 remoteName 与 remote 选项)。
    30  func buildPushArgs(target, branch string, setUpstream bool) []string {
    31  	args := []string{"push"}
    32  	if setUpstream && target != "" {
    33  		args = append(args, "--set-upstream")
    34  	}
    35  	if target != "" {
    36  		args = append(args, target)
    37  	}
    38  	if branch != "" {
    39  		args = append(args, branch)
    40  	}
    41  	return args
    42  }
    43  
    44  // buildPullArgs 构造 git pull 命令的参数列表。
    45  // target 为最终使用的 remote 名。
    46  func buildPullArgs(target, branch string) []string {
    47  	args := []string{"pull"}
    48  	if target != "" {
    49  		args = append(args, target)
    50  	}
    51  	if branch != "" {
    52  		args = append(args, branch)
    53  	}
    54  	return args
    55  }
    56  
    57  // buildRemoteAddArgs 构造 git remote add 命令的参数列表。
    58  func buildRemoteAddArgs(name, url string) ([]string, error) {
    59  	if name == "" || url == "" {
    60  		return nil, &InvalidArgumentError{Msg: "Both remote name and URL are required to add a git remote."}
    61  	}
    62  	return []string{"remote", "add", name, url}, nil
    63  }
    64  
    65  // buildRemoteSetURLArgs 构造 git remote set-url 命令的参数列表。
    66  func buildRemoteSetURLArgs(name, url string) []string {
    67  	return []string{"remote", "set-url", name, url}
    68  }
    69  
    70  // buildRemoteGetURLArgs 构造 git remote get-url 命令的参数列表。
    71  func buildRemoteGetURLArgs(name string) []string {
    72  	return []string{"remote", "get-url", name}
    73  }
    74  
    75  // buildHasUpstreamArgs 构造 upstream 检查命令的参数列表。
    76  func buildHasUpstreamArgs() []string {
    77  	return []string{"rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"}
    78  }
    79  
    80  // buildStatusArgs 构造 git status 命令的参数列表。
    81  func buildStatusArgs() []string {
    82  	return []string{"status", "--porcelain=1", "-b"}
    83  }
    84  
    85  // buildBranchesArgs 构造 git branch 列表命令的参数列表。
    86  func buildBranchesArgs() []string {
    87  	return []string{"branch", "--format=%(refname:short)\t%(HEAD)"}
    88  }
    89  
    90  // buildCreateBranchArgs 构造 git checkout -b 命令的参数列表。
    91  func buildCreateBranchArgs(branch string) []string {
    92  	return []string{"checkout", "-b", branch}
    93  }
    94  
    95  // buildCheckoutBranchArgs 构造 git checkout 命令的参数列表。
    96  func buildCheckoutBranchArgs(branch string) []string {
    97  	return []string{"checkout", branch}
    98  }
    99  
   100  // buildDeleteBranchArgs 构造 git branch 删除命令的参数列表。
   101  func buildDeleteBranchArgs(branch string, force bool) []string {
   102  	flag := "-d"
   103  	if force {
   104  		flag = "-D"
   105  	}
   106  	return []string{"branch", flag, branch}
   107  }
   108  
   109  // buildAddArgs 构造 git add 命令的参数列表。
   110  func buildAddArgs(files []string, all bool) []string {
   111  	args := []string{"add"}
   112  	if len(files) == 0 {
   113  		if all {
   114  			args = append(args, "-A")
   115  		} else {
   116  			args = append(args, ".")
   117  		}
   118  		return args
   119  	}
   120  	args = append(args, "--")
   121  	args = append(args, files...)
   122  	return args
   123  }
   124  
   125  // buildCommitArgs 构造 git commit 命令的参数列表。
   126  func buildCommitArgs(message, authorName, authorEmail string, allowEmpty bool) []string {
   127  	args := []string{"commit", "-m", message}
   128  	if allowEmpty {
   129  		args = append(args, "--allow-empty")
   130  	}
   131  	var prefix []string
   132  	if authorName != "" {
   133  		prefix = append(prefix, "-c", "user.name="+authorName)
   134  	}
   135  	if authorEmail != "" {
   136  		prefix = append(prefix, "-c", "user.email="+authorEmail)
   137  	}
   138  	if len(prefix) > 0 {
   139  		return append(prefix, args...)
   140  	}
   141  	return args
   142  }
   143  
   144  // allowedResetModes 是 git reset 允许的模式集合。
   145  var allowedResetModes = map[GitResetMode]struct{}{
   146  	GitResetModeSoft:  {},
   147  	GitResetModeMixed: {},
   148  	GitResetModeHard:  {},
   149  	GitResetModeMerge: {},
   150  	GitResetModeKeep:  {},
   151  }
   152  
   153  // buildResetArgs 构造 git reset 命令的参数列表。
   154  // git reset 的两种用法互斥:带 mode 时重置 HEAD/索引/工作区,带 paths 时仅取消暂存路径。
   155  func buildResetArgs(mode GitResetMode, target string, paths []string) ([]string, error) {
   156  	if mode != "" {
   157  		if _, ok := allowedResetModes[mode]; !ok {
   158  			return nil, &InvalidArgumentError{Msg: "Reset mode must be one of soft, mixed, hard, merge, keep."}
   159  		}
   160  	}
   161  	if mode != "" && len(paths) > 0 {
   162  		return nil, &InvalidArgumentError{Msg: "Reset mode and paths cannot be used together."}
   163  	}
   164  	args := []string{"reset"}
   165  	if mode != "" {
   166  		args = append(args, "--"+string(mode))
   167  	}
   168  	if target != "" {
   169  		args = append(args, target)
   170  	}
   171  	if len(paths) > 0 {
   172  		args = append(args, "--")
   173  		args = append(args, paths...)
   174  	}
   175  	return args, nil
   176  }
   177  
   178  // buildRestoreArgs 构造 git restore 命令的参数列表。
   179  // 与原生 git 对齐:未显式指定标志时默认仅恢复工作区;任一标志显式 false、另一标志未指定时
   180  // 也退化到默认 worktree,避免对调用方抛"必须有一个为 true"的语义错。显式两者都为 false 才报错。
   181  func buildRestoreArgs(paths []string, staged, worktree *bool, source string) ([]string, error) {
   182  	if len(paths) == 0 {
   183  		return nil, &InvalidArgumentError{Msg: "At least one path is required."}
   184  	}
   185  
   186  	stagedOn := staged != nil && *staged
   187  	worktreeOn := worktree != nil && *worktree
   188  	if !stagedOn && !worktreeOn && (staged == nil || worktree == nil) {
   189  		worktreeOn = true
   190  	}
   191  
   192  	if !stagedOn && !worktreeOn {
   193  		return nil, &InvalidArgumentError{Msg: "At least one of staged or worktree must be true."}
   194  	}
   195  
   196  	args := []string{"restore"}
   197  	if worktreeOn {
   198  		args = append(args, "--worktree")
   199  	}
   200  	if stagedOn {
   201  		args = append(args, "--staged")
   202  	}
   203  	if source != "" {
   204  		args = append(args, "--source", source)
   205  	}
   206  	args = append(args, "--")
   207  	args = append(args, paths...)
   208  	return args, nil
   209  }
   210  
   211  // clonePlan 描述一次 clone 的完整执行计划。
   212  type clonePlan struct {
   213  	// args 是 git clone 命令的参数列表。
   214  	args []string
   215  	// repoPath 是用于 post-clone 调整的仓库路径。
   216  	repoPath string
   217  	// sanitizedURL 是凭证剥离后的 URL,用于 clone 后重置 origin。
   218  	sanitizedURL string
   219  	// shouldStrip 表示是否需要在 clone 完成后重置 origin URL。
   220  	shouldStrip bool
   221  }
   222  
   223  // buildClonePlan 构造 clone 命令的参数与凭证剥离元信息。
   224  func buildClonePlan(rawURL, path, branch string, depth int, username, password string, dangerouslyStore bool) (*clonePlan, error) {
   225  	// 调用方直接把凭证嵌入 URL 时也强制要求 https,与 withCredentials 注入路径保持一致的安全边界,
   226  	// 避免 `http://user:pass@host/...` 这类绕过明文走 HTTP。
   227  	if err := requireHTTPSIfHasCredentials(rawURL); err != nil {
   228  		return nil, err
   229  	}
   230  	cloneURL := rawURL
   231  	if username != "" && password != "" {
   232  		var err error
   233  		cloneURL, err = withCredentials(rawURL, username, password)
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  	}
   238  	sanitized := stripCredentials(cloneURL)
   239  	shouldStrip := !dangerouslyStore && sanitized != cloneURL
   240  
   241  	repoPath := path
   242  	if shouldStrip {
   243  		if repoPath == "" {
   244  			repoPath = deriveRepoDirFromURL(rawURL)
   245  		}
   246  		if repoPath == "" {
   247  			return nil, &InvalidArgumentError{Msg: "A destination path is required when using credentials without storing them."}
   248  		}
   249  	}
   250  
   251  	args := []string{"clone", cloneURL}
   252  	if branch != "" {
   253  		args = append(args, "--branch", branch, "--single-branch")
   254  	}
   255  	if depth > 0 {
   256  		args = append(args, "--depth", strconv.Itoa(depth))
   257  	}
   258  	if path != "" {
   259  		args = append(args, path)
   260  	}
   261  
   262  	plan := &clonePlan{args: args, repoPath: repoPath}
   263  	if shouldStrip {
   264  		plan.sanitizedURL = sanitized
   265  		plan.shouldStrip = true
   266  	}
   267  	return plan, nil
   268  }