update engine

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