> --exclude='.git/objects' 的备份恢复出来的是尸体,不是仓库
一条每晚跑的 rsync 备份规则,为了省体积排除了对象库:
结论先行¶
一条每晚跑的 rsync 备份规则,为了省体积排除了对象库:
--exclude='.git/objects'
备份里于是留下一个有 HEAD、有 refs/、有 config、没有任何对象的
.git 空壳。恢复出来 git 直接拒绝打开 —— 它看起来是个仓库,
ls -a 看得到 .git,但它一行历史都没有。
全机扫一遍,52 个仓库处于这个状态。
最关键的一步不是发现它们坏了,是证明它们是从这份备份恢复回来的:
每个坏仓库 .git 目录的 mtime 都停在该项目各自最后活跃的那一天,
而不是集中在某次恢复操作的时间。rsync -a 保留时间戳 —— 时间戳的
这种"散开"分布,只有"逐个从一份保留 mtime 的备份里拷回来"能产生。
善后按历史的真实去向分三类(不能一视同仁):
| 情况 | 数量 | 处理 |
|---|---|---|
| 有 GitHub remote | 31 | 从远端恢复真实历史 |
| 历史确已丢失 | 15 | 重新 init,推私有远端 |
| 第三方 clone | 12 | 移除空壳,上游地址记进 THIRDPARTY_UPSTREAMS.txt |
修法两条:规则改成 --exclude='.git'(备份只存工作树,恢复出来是
干净目录而不是坏壳),历史保护交给 remote —— 原先14 个仓库根本没有远端,
也就是说这份坏备份曾是它们历史的唯一副本。
为什么¶
.git/objects 是仓库里唯一不可从别处推导的部分。
工作树是 HEAD 那一次 commit 的投影,refs/ 是指针,config 是配置,
只有对象库装着内容本身。排掉它,等于保留了全部索引结构、丢掉全部数据 ——
而索引结构恰好是让它"看起来还在"的那一半。
这比"备份里没有这个仓库"更危险,而不是更安全:
- 缺目录会在恢复时立刻、显眼地失败;坏壳要等到你
git log才失败, 而那可能是半年后。 - 坏壳会让"我有备份"这个判断保持为真。它过的是存在性检查, 不过可用性检查。
"省体积"的动机也值得算一遍。对象库是 git 里压缩得最好的部分
(zlib + delta + packfile),工作树反而是展开的。排掉 objects 省下的,
通常远小于它放弃的东西 —— 这是个连收益都很小的权衡。
还有一层只有把两件事并排看才成立的判断:这台机器上另有一份每晚
rsync 整棵树、保留 30 天 daily + 12 周 weekly + 永久 monthly 的备份,
落在另一块物理盘上。它也 --exclude='.git'。所以:
它保住文件,丢掉历史,而且两块盘在同一台机器里,同一场火里一起没。
一份排除 .git 的异盘备份和一个 git remote 不互相替代:前者给你
"昨天的文件",后者给你"可以 diff 的历史"和"离机副本"。
缺哪一个都不是冗余,是缺口。
怎么用¶
现在就查一遍自己有多少个坏壳,一条命令:
find ~ -type d -name '.git' -prune 2>/dev/null | while read -r g; do
[ -d "$g/objects" ] && [ -n "$(ls -A "$g/objects" 2>/dev/null)" ] || echo "EMPTY: $g"
done
更严格一点,用 git 自己判(能捕捉 objects 非空但 pack 损坏的情况):
git -C "$repo" rev-parse HEAD >/dev/null 2>&1 || echo "BROKEN: $repo"
备份排除项的判据:排掉之后恢复的结果,是"缺东西"还是"坏东西"。
缺东西可以接受(下次 build 能再生)。坏东西不行 —— 它会继续冒充完好。
node_modules/、__pycache__/、target/、.venv/ 属于前者;
.git/objects、数据库的 WAL 段、索引的一半分片属于后者。
要么整个排掉,要么整个留下,不要排掉一个结构的内脏。
mtime 是免费的取证工具。 一批文件的时间戳是"集中在某一刻"还是 "散开在各自的历史里",能区分"被某次操作统一改过"和"从保留时间戳的 副本拷回来的"。排查数据来源时先看一眼 mtime 的分布,比读日志便宜得多。
仓库数量和远端数量要对账。 一个没有远端的本地仓库,它的历史 只有一个副本,而备份策略通常没把它当数据对待("代码在 GitHub 上" 是个默认假设,对 14 个仓库来说是假的):
find ~ -type d -name '.git' -prune | while read -r g; do
git -C "$(dirname "$g")" remote -v | grep -q . || echo "NO REMOTE: $(dirname "$g")"
done
第三方 clone 要有来源标记。 那 12 个空壳如果直接删掉,就再也不知道
它们原来是谁的;记进一个 name url path 的文本文件,之后谁都能重新 clone,
顺带防止下次有人把它当自研代码去"改进"。
出处¶
~/Engineering/worklog-printer/README.md §0「已修:备份规则产出坏仓库
(2026-08-03)」与 .gitignore 顶部关于 /data/EngineeringBackup 的说明。
那 12 个第三方条目现在在 ~/Engineering/THIRDPARTY_UPSTREAMS.txt
里(2026-10-11 核对,正好 12 行,与记录一致)。
相关:进 git 的判据只有一句:明天这个仓库能不能把它重算出来。