From be99e73a0ecb3927fd112bbc0d8c6c78a5e87b8f Mon Sep 17 00:00:00 2001 From: tamakyi Date: Tue, 8 Sep 2026 17:31:52 +0800 Subject: [PATCH] =?UTF-8?q?git-sync-web=20=E8=87=AA=E5=8A=A8=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=202026-09-08=2017:31:52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- engine.go | 56 +++++++++++++++++++++++++++++++++-------- git.go | 6 +++++ logs/git-auto-sync.log | 7 ++++++ stats/git-auto-sync.csv | 1 + 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/engine.go b/engine.go index 2c853da..7abe694 100644 --- a/engine.go +++ b/engine.go @@ -393,7 +393,7 @@ func (e *Engine) SyncNow(name string) error { if s.syncing { return fmt.Errorf("正在同步中,请稍候") } - go e.syncOnce(s, "手动触发") + go e.safeSync(s, "手动触发") return nil } @@ -446,7 +446,8 @@ func (e *Engine) setOK(s *repoState, detail string) { } // ensureRemotes 把配置中的远程同步到仓库(缺失则添加,URL 变化则更新) -func (e *Engine) ensureRemotes(s *repoState, cfg *RepoConfig) { for _, r := range cfg.Remotes { +func (e *Engine) ensureRemotes(s *repoState, cfg *RepoConfig) { + for _, r := range cfg.Remotes { out, err := gitRun(cfg.RepoPath, 15*time.Second, "remote", "get-url", r.Name) if err != nil { if _, aerr := gitRun(cfg.RepoPath, 15*time.Second, "remote", "add", r.Name, r.URL); aerr != nil { @@ -476,6 +477,22 @@ func (e *Engine) refreshCounts(s *repoState, cfg *RepoConfig, remote string) { e.pushStatus(s) } +// safeSync 包装 syncOnce:捕获后台 goroutine 的 panic,避免整个进程被杀死。 +// 此前「立即同步后程序自动退出」的直接原因就是后台 goroutine panic 无 recover。 +func (e *Engine) safeSync(s *repoState, trigger string) { + defer func() { + if r := recover(); r != nil { + stack := string(debug.Stack()) + if len(stack) > 4096 { + stack = stack[:4096] + "\n…(堆栈已截断)" + } + log.Printf("[panic] syncOnce(%s): %v\n%s", s.cfg.Name, r, stack) + e.logf(s, "error", "同步发生内部异常(已恢复,进程继续运行): %v", r) + } + }() + e.syncOnce(s, trigger) +} + func (e *Engine) syncOnce(s *repoState, trigger string) { s.mu.Lock() if s.syncing { @@ -511,6 +528,32 @@ func (e *Engine) syncOnce(s *repoState, trigger string) { return } + // 分支对齐:本地当前分支与配置不一致时,自动切换/创建(对齐原 bash 脚本行为) + // 否则 push 会因 src refspec 不存在而失败 + if cur, cerr := currentBranch(cfg.RepoPath); cerr == nil && cur != cfg.Branch { + switch { + case branchExists(cfg.RepoPath, cfg.Branch): + 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) + } + case remoteRefExists(cfg.RepoPath, remote, cfg.Branch): + if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-B", cfg.Branch, remote+"/"+cfg.Branch); err != nil { + e.logf(s, "warn", "基于 %s/%s 创建分支失败: %v(继续在 %s 上同步)", remote, cfg.Branch, err, cur) + } else { + _, _ = gitRun(cfg.RepoPath, 15*time.Second, "branch", "--set-upstream-to", remote+"/"+cfg.Branch, cfg.Branch) + e.logf(s, "info", "本地无分支 %s,已基于 %s/%s 创建并跟踪", cfg.Branch, remote, cfg.Branch) + } + 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 { + e.logf(s, "info", "本地与远程均无分支 %s,已基于当前 HEAD(%s)创建", cfg.Branch, cur) + } + } + } + dirty := dirtyCount(cfg.RepoPath) if dirty > 0 { if cfg.AutoCommit { @@ -551,18 +594,11 @@ func (e *Engine) syncOnce(s *repoState, trigger string) { s.mu.Unlock() e.pushStatus(s) - aborted := false - if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") { - if !e.pushAll(s, cfg, ahead) { - aborted = true // 推送失败,本轮终止,避免继续拉取加剧分叉 - } - } - if !aborted && behind > 0 && (cfg.Mode == "pull" || cfg.Mode == "both") { + if behind > 0 && (cfg.Mode == "pull" || cfg.Mode == "both") { if !e.pullRemote(s, cfg, remote, behind) { return } } - ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch) now := time.Now().Format("2006-01-02 15:04:05") s.mu.Lock() diff --git a/git.go b/git.go index fec07ce..6e513f4 100644 --- a/git.go +++ b/git.go @@ -72,6 +72,12 @@ func remoteRefExists(path, remote, branch string) bool { 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 { diff --git a/logs/git-auto-sync.log b/logs/git-auto-sync.log index 17f20c3..baf5483 100644 --- a/logs/git-auto-sync.log +++ b/logs/git-auto-sync.log @@ -73,3 +73,10 @@ error: failed to push some refs to 'https://git.shiroko.one/tamakyi/github-auto- [2026-09-08 17:28:21] === 同步完成(本地领先 0,落后 0)=== [2026-09-08 17:29:22] === 开始同步(定时检查,模式 push)=== [2026-09-08 17:29:30] 检测到 2 个未提交改动,执行自动提交 +[2026-09-08 17:30:38] 自动提交完成,将随本轮同步推送 +[2026-09-08 17:30:40] 远程分支 origin/main 不存在(远程仓库可能为空或分支名不一致),将推送本地 11 个提交以建立远程分支 +[2026-09-08 17:30:42] 推送至 origin 失败: error: src refspec main does not match any +error: failed to push some refs to 'https://git.shiroko.one/tamakyi/github-auto-sync-golang' +[2026-09-08 17:30:43] === 同步完成(本地领先 0,落后 0)=== +[2026-09-08 17:31:43] === 开始同步(定时检查,模式 push)=== +[2026-09-08 17:31:51] 检测到 4 个未提交改动,执行自动提交 diff --git a/stats/git-auto-sync.csv b/stats/git-auto-sync.csv index 705e3b2..c4f048a 100644 --- a/stats/git-auto-sync.csv +++ b/stats/git-auto-sync.csv @@ -9,3 +9,4 @@ timestamp,action,result,commits 2026-09-08 17:25:49,push,failed,8 2026-09-08 17:27:05,push,failed,9 2026-09-08 17:28:21,push,failed,10 +2026-09-08 17:30:42,push,failed,11