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 }