package git import ( "bytes" "context" "fmt" "os/exec" "strconv" "strings" "time" ) type BranchSyncStatus struct { Behind int DevBranch string AutoBuildBranch string Unavailable bool Reason string } type BranchSyncOpts struct { WorkspaceRoot string DevBranch string AutoBuildBranch string } // runGit runs git -C workspaceRoot and returns stdout, stderr, error. func runGit(ctx context.Context, workspaceRoot string, args ...string) (string, string, error) { cmd := exec.CommandContext(ctx, "git", append([]string{"-C", workspaceRoot}, args...)...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err } // CheckBranchSync returns the sync status between devBranch and autoBuildBranch. func CheckBranchSync(ctx context.Context, opts BranchSyncOpts) (BranchSyncStatus, error) { if opts.DevBranch == opts.AutoBuildBranch { return BranchSyncStatus{ Unavailable: true, Reason: "开发分支与自动化构建分支相同", }, nil } checkCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if _, _, err := runGit(checkCtx, opts.WorkspaceRoot, "rev-parse", "--is-inside-work-tree"); err != nil { return BranchSyncStatus{Unavailable: true, Reason: fmt.Sprintf("非 git 仓库: %v", err)}, nil } fetchCtx, fetchCancel := context.WithTimeout(ctx, 30*time.Second) defer fetchCancel() if _, stderr, err := runGit(fetchCtx, opts.WorkspaceRoot, "fetch", "origin", opts.DevBranch, opts.AutoBuildBranch); err != nil { return BranchSyncStatus{Unavailable: true, Reason: fmt.Sprintf("fetch 失败: %s", stderr)}, nil } revRange := fmt.Sprintf("origin/%s..origin/%s", opts.AutoBuildBranch, opts.DevBranch) out, _, err := runGit(ctx, opts.WorkspaceRoot, "rev-list", "--count", revRange) if err != nil { return BranchSyncStatus{Unavailable: true, Reason: fmt.Sprintf("rev-list 失败: %v", err)}, nil } behind, err := strconv.Atoi(out) if err != nil { return BranchSyncStatus{Unavailable: true, Reason: fmt.Sprintf("解析提交数失败: %v", err)}, nil } return BranchSyncStatus{ Behind: behind, DevBranch: opts.DevBranch, AutoBuildBranch: opts.AutoBuildBranch, }, nil } // RunBranchSync pushes devBranch HEAD to autoBuildBranch (no --force). func RunBranchSync(ctx context.Context, opts BranchSyncOpts) error { fetchCtx, fetchCancel := context.WithTimeout(ctx, 30*time.Second) defer fetchCancel() if _, stderr, err := runGit(fetchCtx, opts.WorkspaceRoot, "fetch", "origin", opts.DevBranch, opts.AutoBuildBranch); err != nil { return fmt.Errorf("fetch 失败: %s: %w", stderr, err) } refspec := fmt.Sprintf("origin/%s:refs/heads/%s", opts.DevBranch, opts.AutoBuildBranch) if _, stderr, err := runGit(ctx, opts.WorkspaceRoot, "push", "origin", refspec); err != nil { return fmt.Errorf("push 失败: %s: %w", stderr, err) } return nil } // GitFetch runs git fetch origin in workspaceRoot, best-effort. func GitFetch(ctx context.Context, workspaceRoot string) error { _, _, err := runGit(ctx, workspaceRoot, "fetch", "origin") return err } // DeleteLocalBranch deletes a local branch if it exists. func DeleteLocalBranch(ctx context.Context, workspaceRoot, branch string) error { _, _, err := runGit(ctx, workspaceRoot, "show-ref", "--verify", "refs/heads/"+branch) if err != nil { // Branch does not exist; no-op. return nil } _, stderr, err := runGit(ctx, workspaceRoot, "branch", "-D", branch) if err != nil { return fmt.Errorf("delete branch %s: %s: %w", branch, stderr, err) } return nil }