github.com/qiniu/go-sdk/v7@v7.27.0/examples/sandbox_git/main.go (about) 1 // sandbox_git 演示如何使用 Sandbox 的 Git 高层接口完成常见 git 操作。 2 // 3 // 本示例完全在沙箱内部进行,不依赖外部仓库:通过在沙箱内本地建立一个 4 // bare 仓库当作 "remote",串起 Init / Add / Commit / Push / Pull 闭环。 5 // 如果设置了 QINIU_GIT_REPO_URL(HTTPS)、QINIU_GIT_USERNAME、 6 // QINIU_GIT_PASSWORD 环境变量,会额外演示带凭证的 Clone。 7 package main 8 9 import ( 10 "context" 11 "fmt" 12 "log" 13 "os" 14 "time" 15 16 "github.com/qiniu/go-sdk/v7/sandbox" 17 ) 18 19 func main() { 20 apiKey := os.Getenv("QINIU_API_KEY") 21 if apiKey == "" { 22 log.Fatal("请设置 QINIU_API_KEY 环境变量") 23 } 24 25 apiURL := os.Getenv("QINIU_SANDBOX_API_URL") 26 27 c, err := sandbox.NewClient(&sandbox.Config{ 28 APIKey: apiKey, 29 Endpoint: apiURL, 30 }) 31 if err != nil { 32 log.Fatalf("创建客户端失败: %v", err) 33 } 34 35 ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second) 36 defer cancel() 37 38 // 1. 选取可用模板并创建沙箱 39 templates, err := c.ListTemplates(ctx, nil) 40 if err != nil { 41 log.Fatalf("列出模板失败: %v", err) 42 } 43 var templateID string 44 for _, tmpl := range templates { 45 if tmpl.BuildStatus == sandbox.BuildStatusReady || tmpl.BuildStatus == sandbox.BuildStatusUploaded { 46 templateID = tmpl.TemplateID 47 break 48 } 49 } 50 if templateID == "" { 51 log.Fatal("没有构建成功的模板") 52 } 53 fmt.Printf("使用模板: %s\n", templateID) 54 55 timeout := int32(240) 56 sb, _, err := c.CreateAndWait(ctx, sandbox.CreateParams{ 57 TemplateID: templateID, 58 Timeout: &timeout, 59 }, sandbox.WithPollInterval(2*time.Second)) 60 if err != nil { 61 log.Fatalf("创建沙箱失败: %v", err) 62 } 63 fmt.Printf("沙箱已就绪: %s\n", sb.ID()) 64 65 defer func() { 66 killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second) 67 defer killCancel() 68 if err := sb.Kill(killCtx); err != nil { 69 log.Printf("终止沙箱失败: %v", err) 70 } else { 71 fmt.Printf("沙箱 %s 已终止\n", sb.ID()) 72 } 73 }() 74 75 git := sb.Git() 76 repoPath := "/tmp/demo-repo" 77 bareRepoPath := "/tmp/demo-remote.git" 78 consumerPath := "/tmp/demo-consumer" 79 80 // 2. 初始化工作仓库 81 fmt.Println("\n--- Init ---") 82 if _, err := git.Init(ctx, repoPath, &sandbox.InitOptions{InitialBranch: "main"}); err != nil { 83 log.Fatalf("Init 失败: %v", err) 84 } 85 fmt.Printf("已初始化仓库: %s(initial-branch=main)\n", repoPath) 86 87 // 同时初始化一个 bare 仓库当作 remote 88 if _, err := git.Init(ctx, bareRepoPath, &sandbox.InitOptions{Bare: true, InitialBranch: "main"}); err != nil { 89 log.Fatalf("Init(bare) 失败: %v", err) 90 } 91 fmt.Printf("已初始化 bare 仓库: %s\n", bareRepoPath) 92 93 // 3. 配置提交用户(local scope) 94 fmt.Println("\n--- ConfigureUser / SetConfig / GetConfig ---") 95 if _, err := git.ConfigureUser(ctx, "Sandbox Demo", "demo@example.com", &sandbox.ConfigOptions{ 96 Scope: sandbox.GitConfigScopeLocal, 97 Path: repoPath, 98 }); err != nil { 99 log.Fatalf("ConfigureUser 失败: %v", err) 100 } 101 // SetConfig 演示:单独设置一个非 user 的键 102 if _, err := git.SetConfig(ctx, "core.autocrlf", "input", &sandbox.ConfigOptions{ 103 Scope: sandbox.GitConfigScopeLocal, 104 Path: repoPath, 105 }); err != nil { 106 log.Fatalf("SetConfig 失败: %v", err) 107 } 108 for _, key := range []string{"user.name", "user.email", "core.autocrlf"} { 109 val, err := git.GetConfig(ctx, key, &sandbox.ConfigOptions{ 110 Scope: sandbox.GitConfigScopeLocal, 111 Path: repoPath, 112 }) 113 if err != nil { 114 log.Fatalf("GetConfig(%s) 失败: %v", key, err) 115 } 116 fmt.Printf(" %s = %q\n", key, val) 117 } 118 // 不存在的键返回空字符串 119 missing, err := git.GetConfig(ctx, "user.notexist", &sandbox.ConfigOptions{ 120 Scope: sandbox.GitConfigScopeLocal, 121 Path: repoPath, 122 }) 123 if err != nil { 124 log.Fatalf("GetConfig(missing) 失败: %v", err) 125 } 126 fmt.Printf(" user.notexist = %q(未配置)\n", missing) 127 128 // 4. 写入文件并暂存提交 129 fmt.Println("\n--- Add / Commit ---") 130 if _, err := sb.Files().Write(ctx, repoPath+"/README.md", []byte("# demo\n")); err != nil { 131 log.Fatalf("写入文件失败: %v", err) 132 } 133 if _, err := git.Add(ctx, repoPath, nil); err != nil { // nil → 默认 -A 134 log.Fatalf("Add 失败: %v", err) 135 } 136 if _, err := git.Commit(ctx, repoPath, "feat: initial commit", &sandbox.CommitOptions{ 137 AuthorName: "Sandbox Demo", 138 AuthorEmail: "demo@example.com", 139 }); err != nil { 140 log.Fatalf("Commit 失败: %v", err) 141 } 142 fmt.Println("已创建初始提交") 143 144 // 5. 查看状态 145 fmt.Println("\n--- Status ---") 146 st, err := git.Status(ctx, repoPath, nil) 147 if err != nil { 148 log.Fatalf("Status 失败: %v", err) 149 } 150 fmt.Printf("CurrentBranch=%s, Detached=%v, Clean=%v, Total=%d, Staged=%d, Unstaged=%d\n", 151 st.CurrentBranch, st.Detached, st.IsClean(), st.TotalCount(), st.StagedCount(), st.UnstagedCount()) 152 153 // 6. 分支管理:CreateBranch -> 修改 -> Commit -> Branches -> CheckoutBranch -> DeleteBranch 154 fmt.Println("\n--- Branches ---") 155 if _, err := git.CreateBranch(ctx, repoPath, "feature/x", nil); err != nil { 156 log.Fatalf("CreateBranch 失败: %v", err) 157 } 158 if _, err := sb.Files().Write(ctx, repoPath+"/feature.txt", []byte("hello feature\n")); err != nil { 159 log.Fatalf("写入特性文件失败: %v", err) 160 } 161 if _, err := git.Add(ctx, repoPath, &sandbox.AddOptions{Files: []string{"feature.txt"}}); err != nil { 162 log.Fatalf("Add(files) 失败: %v", err) 163 } 164 if _, err := git.Commit(ctx, repoPath, "feat: add feature.txt", nil); err != nil { 165 log.Fatalf("Commit 失败: %v", err) 166 } 167 168 branches, err := git.Branches(ctx, repoPath, nil) 169 if err != nil { 170 log.Fatalf("Branches 失败: %v", err) 171 } 172 fmt.Printf("分支: %v,当前: %s\n", branches.Branches, branches.CurrentBranch) 173 174 if _, err := git.CheckoutBranch(ctx, repoPath, "main", nil); err != nil { 175 log.Fatalf("CheckoutBranch 失败: %v", err) 176 } 177 if _, err := git.DeleteBranch(ctx, repoPath, "feature/x", &sandbox.DeleteBranchOptions{Force: true}); err != nil { 178 log.Fatalf("DeleteBranch 失败: %v", err) 179 } 180 fmt.Println("已切回 main 并强制删除 feature/x") 181 182 // 7. Reset / Restore 演示 183 fmt.Println("\n--- Reset / Restore ---") 184 185 // 7a. Reset paths-only:取消暂存(不带 mode) 186 if _, err := sb.Files().Write(ctx, repoPath+"/dirty.txt", []byte("dirty\n")); err != nil { 187 log.Fatalf("写入脏文件失败: %v", err) 188 } 189 if _, err := git.Add(ctx, repoPath, nil); err != nil { 190 log.Fatalf("Add 失败: %v", err) 191 } 192 st, err = git.Status(ctx, repoPath, nil) 193 if err != nil { 194 log.Fatalf("Status 失败: %v", err) 195 } 196 fmt.Printf("Add 后 staged=%d,unstaged=%d\n", st.StagedCount(), st.UnstagedCount()) 197 if _, err := git.Reset(ctx, repoPath, &sandbox.ResetOptions{ 198 Paths: []string{"dirty.txt"}, 199 }); err != nil { 200 log.Fatalf("Reset(paths) 失败: %v", err) 201 } 202 st, err = git.Status(ctx, repoPath, nil) 203 if err != nil { 204 log.Fatalf("Status 失败: %v", err) 205 } 206 fmt.Printf("Reset(paths) 后 staged=%d,unstaged=%d\n", st.StagedCount(), st.UnstagedCount()) 207 208 // 7b. Reset --hard:丢弃工作区改动并把 HEAD 重置到指定提交 209 if _, err := sb.Files().Write(ctx, repoPath+"/README.md", []byte("# demo (modified)\n")); err != nil { 210 log.Fatalf("修改 README 失败: %v", err) 211 } 212 if _, err := git.Reset(ctx, repoPath, &sandbox.ResetOptions{ 213 Mode: sandbox.GitResetModeHard, 214 Target: "HEAD", 215 }); err != nil { 216 log.Fatalf("Reset(--hard HEAD) 失败: %v", err) 217 } 218 readme, err := sb.Files().ReadText(ctx, repoPath+"/README.md") 219 if err != nil { 220 log.Fatalf("读取 README 失败: %v", err) 221 } 222 fmt.Printf("Reset --hard 后 README.md = %q\n", readme) 223 224 // 7c. Restore --staged:取消暂存(保留工作区) 225 if _, err := sb.Files().Write(ctx, repoPath+"/README.md", []byte("# demo (staged change)\n")); err != nil { 226 log.Fatalf("修改 README 失败: %v", err) 227 } 228 if _, err := git.Add(ctx, repoPath, nil); err != nil { 229 log.Fatalf("Add 失败: %v", err) 230 } 231 stagedPtr := boolPtr(true) 232 if _, err := git.Restore(ctx, repoPath, &sandbox.RestoreOptions{ 233 Paths: []string{"README.md"}, 234 Staged: stagedPtr, // 仅取消暂存,工作区保留 235 }); err != nil { 236 log.Fatalf("Restore(--staged) 失败: %v", err) 237 } 238 st, err = git.Status(ctx, repoPath, nil) 239 if err != nil { 240 log.Fatalf("Status 失败: %v", err) 241 } 242 fmt.Printf("Restore --staged 后 staged=%d,unstaged=%d\n", st.StagedCount(), st.UnstagedCount()) 243 244 // 7d. Restore --worktree --source=HEAD:把工作区改动恢复到 HEAD 版本 245 if _, err := git.Restore(ctx, repoPath, &sandbox.RestoreOptions{ 246 Paths: []string{"README.md"}, 247 Source: "HEAD", 248 }); err != nil { 249 log.Fatalf("Restore(--source HEAD) 失败: %v", err) 250 } 251 readme, err = sb.Files().ReadText(ctx, repoPath+"/README.md") 252 if err != nil { 253 log.Fatalf("读取 README 失败: %v", err) 254 } 255 fmt.Printf("Restore --source HEAD 后 README.md = %q\n", readme) 256 257 // 8. Remote 管理(含 Overwrite) 258 fmt.Println("\n--- Remote ---") 259 // 先用一个占位 URL 添加 origin 260 if _, err := git.RemoteAdd(ctx, repoPath, "origin", "https://example.com/placeholder.git", nil); err != nil { 261 log.Fatalf("RemoteAdd 失败: %v", err) 262 } 263 // 再用 Overwrite=true 把它改成本地 bare 仓库(用于后面的 push/pull) 264 if _, err := git.RemoteAdd(ctx, repoPath, "origin", bareRepoPath, &sandbox.RemoteAddOptions{ 265 Overwrite: true, 266 }); err != nil { 267 log.Fatalf("RemoteAdd(overwrite) 失败: %v", err) 268 } 269 url, err := git.RemoteGet(ctx, repoPath, "origin", nil) 270 if err != nil { 271 log.Fatalf("RemoteGet 失败: %v", err) 272 } 273 fmt.Printf("origin URL = %s\n", url) 274 275 missingRemote, err := git.RemoteGet(ctx, repoPath, "nonexistent", nil) 276 if err != nil { 277 log.Fatalf("RemoteGet(nonexistent) 失败: %v", err) 278 } 279 fmt.Printf("nonexistent URL = %q(未配置)\n", missingRemote) 280 281 // 9. Push(不带凭证;SetUpstream 默认 true) 282 fmt.Println("\n--- Push ---") 283 if _, err := git.Push(ctx, repoPath, &sandbox.PushOptions{ 284 Remote: "origin", 285 Branch: "main", 286 }); err != nil { 287 log.Fatalf("Push 失败: %v", err) 288 } 289 fmt.Printf("已推送到 %s(main)\n", bareRepoPath) 290 291 // 10. Pull:再 clone 出一个 consumer 仓库,从同一个 bare remote 拉取 292 fmt.Println("\n--- Pull(通过本地 clone 演示)---") 293 // 用 git clone 本地路径作为 remote 来初始化 consumer(无凭证场景) 294 if _, err := git.Clone(ctx, bareRepoPath, &sandbox.CloneOptions{ 295 Path: consumerPath, 296 }); err != nil { 297 log.Fatalf("本地 Clone 失败: %v", err) 298 } 299 fmt.Printf("已 clone 到 %s\n", consumerPath) 300 301 // 在 repoPath 里再做一次提交并 push 302 if _, err := sb.Files().Write(ctx, repoPath+"/CHANGELOG.md", []byte("# v1\n")); err != nil { 303 log.Fatalf("写入 CHANGELOG 失败: %v", err) 304 } 305 if _, err := git.Add(ctx, repoPath, nil); err != nil { 306 log.Fatalf("Add 失败: %v", err) 307 } 308 if _, err := git.Commit(ctx, repoPath, "docs: add CHANGELOG", nil); err != nil { 309 log.Fatalf("Commit 失败: %v", err) 310 } 311 if _, err := git.Push(ctx, repoPath, &sandbox.PushOptions{ 312 Remote: "origin", 313 Branch: "main", 314 }); err != nil { 315 log.Fatalf("Push(2) 失败: %v", err) 316 } 317 318 // 在 consumer 里 Pull 拉取最新提交 319 if _, err := git.Pull(ctx, consumerPath, &sandbox.PullOptions{ 320 Remote: "origin", 321 Branch: "main", 322 }); err != nil { 323 log.Fatalf("Pull 失败: %v", err) 324 } 325 exists, err := sb.Files().Exists(ctx, consumerPath+"/CHANGELOG.md") 326 if err != nil { 327 log.Fatalf("Exists 失败: %v", err) 328 } 329 fmt.Printf("Pull 后 consumer/CHANGELOG.md 存在 = %v\n", exists) 330 331 // 11. DangerouslyAuthenticate:演示 API 调用形态(仅写到沙箱内的全局 credential store) 332 fmt.Println("\n--- DangerouslyAuthenticate(仅在沙箱内持久化,不影响外部环境)---") 333 if _, err := git.DangerouslyAuthenticate(ctx, &sandbox.AuthenticateOptions{ 334 Username: "demo-user", 335 Password: "demo-token", 336 Host: "example.com", 337 // Protocol 缺省即 "https",这里显式写出仅作演示 338 Protocol: "https", 339 }); err != nil { 340 log.Fatalf("DangerouslyAuthenticate 失败: %v", err) 341 } 342 fmt.Println("已通过 git credential approve 写入 example.com 的凭证") 343 344 // 12. 可选:带凭证的 Clone(仅在 QINIU_GIT_REPO_URL 等环境变量齐备时执行) 345 repoURL := os.Getenv("QINIU_GIT_REPO_URL") 346 username := os.Getenv("QINIU_GIT_USERNAME") 347 password := os.Getenv("QINIU_GIT_PASSWORD") 348 if repoURL != "" && username != "" && password != "" { 349 fmt.Println("\n--- Clone(HTTPS + token,clone 后会自动剥离 origin URL 中的凭证)---") 350 clonePath := "/tmp/cloned-repo" 351 if _, err := git.Clone(ctx, repoURL, &sandbox.CloneOptions{ 352 Path: clonePath, 353 Depth: 1, 354 Username: username, 355 Password: password, 356 }); err != nil { 357 log.Fatalf("Clone 失败: %v", err) 358 } 359 clonedURL, err := git.RemoteGet(ctx, clonePath, "origin", nil) 360 if err != nil { 361 log.Fatalf("RemoteGet(cloned) 失败: %v", err) 362 } 363 fmt.Printf("clone 完成,origin URL = %s(不含凭证)\n", clonedURL) 364 } else { 365 fmt.Println("\n--- 远程 Clone 演示已跳过(未设置 QINIU_GIT_REPO_URL / QINIU_GIT_USERNAME / QINIU_GIT_PASSWORD)---") 366 } 367 } 368 369 // boolPtr 返回 bool 值的指针。 370 func boolPtr(v bool) *bool { return &v }