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 }