diff --git a/engine.go b/engine.go index 9f12aad..a119f20 100644 --- a/engine.go +++ b/engine.go @@ -164,7 +164,10 @@ func (e *Engine) sortedNames() []string { return names } +// FIX: LoadAll 加锁,避免并发读写 map func (e *Engine) LoadAll() { + e.mu.Lock() + defer e.mu.Unlock() for _, name := range e.configNames() { if _, ok := e.states[name]; ok { continue @@ -478,7 +481,6 @@ func (e *Engine) refreshCounts(s *repoState, cfg *RepoConfig, remote string) { } // safeSync 包装 syncOnce:捕获后台 goroutine 的 panic,避免整个进程被杀死。 -// 此前「立即同步后程序自动退出」的直接原因就是后台 goroutine panic 无 recover。 func (e *Engine) safeSync(s *repoState, trigger string) { defer func() { if r := recover(); r != nil { @@ -493,6 +495,7 @@ func (e *Engine) safeSync(s *repoState, trigger string) { e.syncOnce(s, trigger) } +// FIX: 调整同步顺序:先拉取后推送;分支切换避免强制覆盖;pull 使用重试 func (e *Engine) syncOnce(s *repoState, trigger string) { s.mu.Lock() if s.syncing { @@ -528,33 +531,37 @@ func (e *Engine) syncOnce(s *repoState, trigger string) { return } - // 分支对齐:本地当前分支与配置不一致时,自动切换/创建(对齐原 bash 脚本行为) - // 否则 push 会因 src refspec 不存在而失败 + // FIX: 分支对齐逻辑 – 避免使用 -B 强制覆盖,本地存在则普通切换,不存在则创建 if cur, cerr := currentBranch(cfg.RepoPath); cerr == nil && cur != cfg.Branch { tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch) - switch { - case branchExists(cfg.RepoPath, cfg.Branch): + localExists := branchExists(cfg.RepoPath, cfg.Branch) + if localExists { + // 本地分支存在,普通切换(可能因未提交改动而失败) if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", cfg.Branch); err != nil { - e.logf(s, "warn", "切换到配置分支 %s 失败: %v(继续在 %s 上同步)", cfg.Branch, err, cur) - } else { - e.logf(s, "info", "本地分支与配置不一致(%s → %s),已自动切换", cur, cfg.Branch) + e.setFail(s, fmt.Sprintf("切换到配置分支 %s 失败,请先提交或暂存改动: %v", cfg.Branch, err)) + return } - case remoteHasBranch: - if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-B", cfg.Branch, tip); err != nil { - e.logf(s, "warn", "基于远程 %s 创建分支失败: %v(继续在 %s 上同步)", cfg.Branch, err, cur) - } else { + e.logf(s, "info", "本地分支与配置不一致(%s → %s),已自动切换", cur, cfg.Branch) + } else { + // 本地分支不存在,创建(不会覆盖任何内容) + if remoteHasBranch { + if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-b", cfg.Branch, tip); err != nil { + e.setFail(s, fmt.Sprintf("基于远程 %s/%s 创建分支失败: %v", remote, cfg.Branch, err)) + return + } _, _ = gitRun(cfg.RepoPath, 15*time.Second, "branch", "--set-upstream-to", remote+"/"+cfg.Branch, cfg.Branch) e.logf(s, "info", "本地无分支 %s,已基于远程 %s(%s)创建并跟踪", cfg.Branch, cfg.Branch, tip[:8]) - } - default: - if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-B", cfg.Branch); err != nil { - e.logf(s, "warn", "创建分支 %s 失败: %v(继续在 %s 上同步)", cfg.Branch, err, cur) } else { + if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-b", cfg.Branch); err != nil { + e.setFail(s, fmt.Sprintf("创建分支 %s 失败: %v", cfg.Branch, err)) + return + } e.logf(s, "info", "本地与远程均无分支 %s,已基于当前 HEAD(%s)创建", cfg.Branch, cur) } } } + // 自动提交(若有未提交改动) dirty := dirtyCount(cfg.RepoPath) if dirty > 0 { if cfg.AutoCommit { @@ -575,6 +582,7 @@ func (e *Engine) syncOnce(s *repoState, trigger string) { } } + // 计算初始 ahead/behind var ahead, behind int var err error tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch) @@ -585,29 +593,38 @@ func (e *Engine) syncOnce(s *repoState, trigger string) { return } } else { - // 远程分支不存在(空仓库或分支名不一致):把本地提交视为全部领先,推送时建立远程分支 + // 远程分支不存在:认为所有本地提交领先 if out, cerr := gitRun(cfg.RepoPath, 15*time.Second, "rev-list", "--count", "HEAD"); cerr == nil { ahead, _ = strconv.Atoi(strings.TrimSpace(out)) } - e.logf(s, "warn", "远程分支 %s/%s 不存在(远程仓库可能为空或分支名不一致),将推送本地 %d 个提交以建立远程分支", remote, cfg.Branch, ahead) + e.logf(s, "warn", "远程分支 %s/%s 不存在,将推送本地 %d 个提交以建立远程分支", remote, cfg.Branch, ahead) } s.mu.Lock() s.ahead, s.behind, s.dirty = ahead, behind, dirty s.mu.Unlock() e.pushStatus(s) - if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") { - if !e.pushAll(s, cfg, ahead) { - // 推送失败,本轮终止,避免继续拉取加剧分叉 - e.setFail(s, "推送失败,本轮同步已终止(跳过拉取以避免分叉加剧)") - return - } - } + // FIX: 先拉取(pull),再推送(push) if behind > 0 && (cfg.Mode == "pull" || cfg.Mode == "both") { if !e.pullRemote(s, cfg, remote, behind) { return } + // 拉取后重新计算 ahead/behind + ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch) + s.mu.Lock() + s.ahead, s.behind = ahead, behind + s.mu.Unlock() + e.pushStatus(s) } + + if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") { + if !e.pushAll(s, cfg, ahead) { + e.setFail(s, "推送失败") + return + } + } + + // 最终刷新状态 ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch) now := time.Now().Format("2006-01-02 15:04:05") s.mu.Lock() @@ -641,18 +658,22 @@ func (e *Engine) pushAll(s *repoState, cfg *RepoConfig, n int) bool { return allOK } +// FIX: pullRemote 使用 withRetry 包裹主 pull 命令 func (e *Engine) pullRemote(s *repoState, cfg *RepoConfig, remote string, n int) bool { args := []string{"pull"} if cfg.ConflictStrategy == "rebase" { args = append(args, "--rebase") } args = append(args, remote, cfg.Branch, "--no-edit") - _, err := gitRun(cfg.RepoPath, 300*time.Second, args...) + _, err := e.withRetry(s, "pull", func() (string, error) { + return gitRun(cfg.RepoPath, 300*time.Second, args...) + }) if err == nil { e.stat(s, "pull", "success", n) e.logf(s, "info", "拉取成功(%d 个提交)", n) return true } + // 处理冲突 files, cerr := conflictFiles(cfg.RepoPath) if cerr == nil && len(files) > 0 { if cfg.ConflictStrategy == "abort" { @@ -1016,4 +1037,4 @@ func (e *Engine) ReadCommitDiff(name, hash string) (string, error) { out = out[:131072] + "\n…(内容过长已截断)" } return out, nil -} +} \ No newline at end of file