package main import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) // gitRun 在指定仓库目录执行 git 命令,返回合并后的输出。 // 非交互环境:GIT_TERMINAL_PROMPT=0 只能挡住终端提示,挡不住 Windows 凭据管理器(GCM)弹窗, // 因此额外禁用 GCM 交互、askpass 和 ssh 交互,避免后台同步被凭据弹窗永久卡死。 // WaitDelay 保证超时 Kill 后即使 git 的子进程仍占用管道,Wait() 也能强制返回(Windows 上 // CommandContext 只杀主进程,不杀 git-remote-https / 凭据 helper 等子进程)。 func gitRun(repoPath string, timeout time.Duration, args ...string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() full := append([]string{"-C", repoPath}, args...) cmd := exec.CommandContext(ctx, "git", full...) cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=echo", "GCM_INTERACTIVE=never", "GIT_SSH_COMMAND=ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new", ) cmd.WaitDelay = 10 * time.Second var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf err := cmd.Run() out := strings.TrimSpace(buf.String()) if err != nil { if ctx.Err() == context.DeadlineExceeded { return out, fmt.Errorf("命令超时: git %s", strings.Join(args, " ")) } msg := out if msg == "" { msg = err.Error() } return out, errors.New(msg) } return out, nil } func isRepo(path string) bool { if path == "" { return false } _, err := os.Stat(filepath.Join(path, ".git")) return err == nil } func currentBranch(path string) (string, error) { return gitRun(path, 15*time.Second, "rev-parse", "--abbrev-ref", "HEAD") } // aheadBehind 返回 (领先, 落后) 远程跟踪分支的提交数(需先 fetch 才准确) func aheadBehind(path, remote, branch string) (int, int, error) { out, err := gitRun(path, 30*time.Second, "rev-list", "--left-right", "--count", "HEAD..."+remote+"/"+branch) if err != nil { return 0, 0, err } parts := strings.Fields(out) if len(parts) != 2 { return 0, 0, fmt.Errorf("无法解析 rev-list 输出: %q", out) } a, _ := strconv.Atoi(parts[0]) b, _ := strconv.Atoi(parts[1]) return a, b, nil } // remoteRefExists 检查远程跟踪引用是否存在(远程仓库为空或分支名不一致时为 false) func remoteRefExists(path, remote, branch string) bool { _, err := gitRun(path, 15*time.Second, "rev-parse", "--verify", "-q", remote+"/"+branch) return err == nil } // branchExists 检查本地分支是否存在 func branchExists(path, branch string) bool { _, err := gitRun(path, 15*time.Second, "show-ref", "--verify", "-q", "refs/heads/"+branch) return err == nil } func dirtyCount(path string) int { out, err := gitRun(path, 30*time.Second, "status", "--porcelain") if err != nil { return 0 } n := 0 for _, line := range strings.Split(out, "\n") { if strings.TrimSpace(line) != "" { n++ } } return n } var conflictCodes = map[string]bool{ "DD": true, "AU": true, "UD": true, "UA": true, "DU": true, "AA": true, "UU": true, } func conflictFiles(path string) ([]string, error) { out, err := gitRun(path, 30*time.Second, "status", "--porcelain") if err != nil { return nil, err } var files []string seen := map[string]bool{} for _, line := range strings.Split(out, "\n") { if len(line) < 4 { continue } if !conflictCodes[line[:2]] { continue } p := strings.TrimSpace(line[3:]) if i := strings.Index(p, "->"); i >= 0 { p = strings.TrimSpace(p[i+2:]) } if p != "" && !seen[p] { seen[p] = true files = append(files, p) } } return files, nil } func abortMerge(path string) (string, error) { if out, err := gitRun(path, 30*time.Second, "rebase", "--abort"); err == nil { return out, nil } return gitRun(path, 30*time.Second, "merge", "--abort") } // resolveDone 在用户网页上标记"已解决"后调用:暂存全部 → 继续 merge/rebase 流程 func resolveDone(path string, rebase bool) error { if _, err := gitRun(path, 30*time.Second, "add", "-A"); err != nil { return err } if rebase { _, err := gitRun(path, 120*time.Second, "-c", "core.editor=true", "rebase", "--continue") return err } if _, err := gitRun(path, 15*time.Second, "rev-parse", "-q", "--verify", "MERGE_HEAD"); err == nil { _, err := gitRun(path, 120*time.Second, "commit", "--no-edit") return err } return nil }