tools-sync: 增加 ls-remote 同步闸门,并记下 gitea 空 pack 故障的排查法

这次同步事故的教训不是服务器坏了,而是坏了没人知道:pull --rebase 一失败脚本
就退出,push 从没走到,3 个 commit(含整个 bmem skill)在本机躺了 8 天,报告里
却算作"已知故障、待恢复"。而 ls-remote 这 8 天一直是好的。

- sync_repos.ps1 增 Assert-RemoteInSync:push 后(及 -DryRun 时)用 ls-remote 比对
  origin/<branch> 的实际 sha,不一致就 FAIL 并报出滞留/落后的 commit 数。
  远端分支不存在时单独报错,避免那两个计数被静默地按 HEAD 算出误导性数字。
- SKILL.md 阶段 1 写明闸门是硬性的:服务端故障是原因不是结果,不得报成已同步。
- SKILL.md 新增 bad pack header / early EOF 的排查阶梯(三步各排掉一层),并记下
  根因:全局 gitconfig 被注入 [uploadpack] packObjectsHook,把 pack-objects 换成
  了空转脚本。附带提醒 HOME 是 /data/gitea/home 而非 /data/git,以及这是一条
  完整 RCE 链、要连 hook 与 /tmp 载荷一起清。
- bmem/SKILL.md 的"已知边界"改为已修复,避免继续把普通的没 pull 误判成服务端故障。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tMFjKGgxdmxJ28dyXdx1u
This commit is contained in:
Myfro9
2026-09-02 14:57:50 -05:00
co-authored by Claude Opus 5
parent 7d81f59ee0
commit e39e852853
3 changed files with 74 additions and 7 deletions
+43 -3
View File
@@ -56,16 +56,56 @@ if ($st.Out) {
Write-Host "working tree clean"
}
if (-not $DryRun) {
# The sync is only "done" when the remote branch actually points at our commit.
# ls-remote asks for refs only, so it keeps working even when the server's pack
# service is broken -- which is exactly the outage this gate exists to survive.
function Assert-RemoteInSync {
param([string]$Dir, [string]$Branch)
$local = Invoke-Git $Dir @("rev-parse", $Branch)
if ($local.Code -ne 0) { Fail "cannot resolve local $Branch." }
$ls = Invoke-Git $Dir @("ls-remote", "origin", "refs/heads/$Branch")
if ($ls.Code -ne 0) {
Write-Host $ls.Out
Fail "ls-remote failed: cannot verify the push landed. Treat this run as NOT synced."
}
$remote = ($ls.Out -split "\s+")[0]
if ($remote -ne $local.Out) {
Write-Host " local $Branch = $($local.Out)"
if (-not $remote) {
# Empty ref: the counts below would silently be measured against HEAD.
Fail "origin has no refs/heads/$Branch. Nothing on this machine is backed up by the server."
}
Write-Host " remote $Branch = $remote"
$ahead = Invoke-Git $Dir @("rev-list", "--count", "$remote..$($local.Out)")
if ($ahead.Code -eq 0 -and [int]$ahead.Out -gt 0) {
Write-Host " $($ahead.Out) commit(s) STRANDED on this machine - they exist nowhere else."
}
$behind = Invoke-Git $Dir @("rev-list", "--count", "$($local.Out)..$remote")
if ($behind.Code -eq 0 -and [int]$behind.Out -gt 0) {
Write-Host " $($behind.Out) commit(s) from other machines not merged here."
}
Fail "primary is NOT in sync with origin/$Branch. A server-side outage is a reason, never an outcome: do not report this run as synced."
}
Write-Host "verified: origin/$Branch = $remote (this machine's work is on the server)"
}
if ($DryRun) {
# Read-only, but still worth running: silent drift is what this gate is for.
Assert-RemoteInSync $ClaudeSkills $Branch
} else {
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
if ($r.Code -ne 0) {
Write-Host $r.Out
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run."
$stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch")
if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) {
Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed."
}
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run. If the transport itself is broken (see skill Phase 1), fix the server -- do not leave commits stranded."
}
Write-Host $r.Out
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
Write-Host "primary in sync with origin/$Branch"
Assert-RemoteInSync $ClaudeSkills $Branch
}
# ---------- Phase 2: junction reconciliation (~\.codex\skills) ----------