package main import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) 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 outBuf, errBuf bytes.Buffer cmd.Stdout = &outBuf cmd.Stderr = &errBuf err := cmd.Run() out := strings.TrimRight(outBuf.String(), "\r\n\t ") if err != nil { if ctx.Err() == context.DeadlineExceeded { return out, fmt.Errorf("命令超时: git %s", strings.Join(args, " ")) } msg := strings.TrimSpace(errBuf.String()) if msg == "" { 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") } 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 } func remoteRefExists(path, remote, branch string) bool { _, err := gitRun(path, 15*time.Second, "rev-parse", "--verify", "-q", remote+"/"+branch) return err == nil } func branchExists(path, branch string) bool { _, err := gitRun(path, 15*time.Second, "show-ref", "--verify", "-q", "refs/heads/"+branch) return err == nil } func remoteTip(path, remote, branch string) (sha string, exists bool) { if remoteRefExists(path, remote, branch) { out, err := gitRun(path, 15*time.Second, "rev-parse", remote+"/"+branch) if err == nil { return strings.TrimSpace(out), true } } out, err := gitRun(path, 30*time.Second, "ls-remote", remote, "refs/heads/"+branch) if err != nil { return "", false } fields := strings.Fields(strings.TrimSpace(out)) if len(fields) < 2 { return "", false } return fields[0], true } func aheadBehindSha(path, tip string) (int, int, error) { out, err := gitRun(path, 30*time.Second, "rev-list", "--left-right", "--count", "HEAD..."+tip) 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 } 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") } 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 } func changedFiles(path string) map[string]string { out, err := gitRun(path, 30*time.Second, "status", "--porcelain") m := map[string]string{} if err != nil { return m } for _, line := range strings.Split(out, "\n") { if len(line) < 4 { continue } code := line[:2] p := strings.TrimSpace(line[3:]) if i := strings.Index(p, "->"); i >= 0 { p = strings.TrimSpace(p[i+2:]) } p = strings.Trim(p, `"`) if p == "" { continue } switch { case code == "??": m[p] = "?" case code[0] == 'A' || code[0] == 'C': m[p] = "A" case code[0] == 'D' || code[1] == 'D': m[p] = "D" default: m[p] = "M" // 含 UU/AU 冲突状态,统一显示为已修改 } } return m }