# 不把核心代码明文交给 GitHub：我的 Win/Mac 私有 Git、加密云备份与移动开发困境

> Canonical: https://vxi.me/posts/git-backup-win-mac-bare-repo-disaster-recovery/
> Locale: zh-cn
> Published: 2026-10-15
> Updated: 2026-10-16
> Markdown: https://vxi.me/posts/git-backup-win-mac-bare-repo-disaster-recovery.md

最初搭这套 Windows / Mac Git 系统时，我最在意的不是备份速度，也不是省一笔 GitHub 费用，而是一个更早就定下的前提：**长期投入、具有商业价值的核心源代码，不以明文形式交给任何第三方云平台保管。** 即使仓库标着 Private，这条原则也不变。

我不是在断言某家平台会私自拿走代码。风险恰恰在于，我无法把未来很多年的商业资产押在“平台永远没有内部越权、漏洞、供应链攻击、账户泄露或服务侧安全事故”这个假设上。Private 控制的是谁被授权访问，**不等于服务商在技术上拿不到原始 Git 对象**。我希望由自己掌握解密密钥，云端只接触密文；出了问题，至少不会因为一个云账号被攻破就直接交出完整源码。

这才解释了为什么我让 Windows 的 bare repo 做本地 Central，Mac 通过 SMB 同步开发，而不是从一开始就把核心项目推到 GitHub 私有仓。后来加的 E 盘 mirror、移动硬盘 7z 归档和 Cryptomator + OneDrive / Google Drive，首先是为了在不松动明文边界的前提下实现异地容灾。**不过，能把密文备份上云，并不等于已经拥有随时可用的“加密 Git 远程仓库”**；现在手机端想借助 ChatGPT/Codex 协助开发时，这道区别尤其明显。

下面保留 2024–2026 年的操作记录及 Git 2.47.3 小仓库实测；加密 Git remote 和手机端方案仍属于设计讨论，不混作运行结果。

## 为什么不直接用 GitHub Private 托管核心代码

这里的“不信任”是架构上的威胁模型，不是对 GitHub、OneDrive 或 Google Drive 的指控。GitHub 对 Private 仓库提供访问控制和保密承诺，但其[服务条款也规定了若干服务侧访问例外](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#e-private-repositories)。对一般项目，这可能足够；对希望**云端运营方也无法解密源码**的商业项目，就不是同一种保护。

我需要同时满足两个要求：日常仍然像 Git 一样提交、回滚、切分支和在 Win/Mac 间同步；即使整套本地设备丢失，也能从异地找回一份**加密后**的历史。于是信任边界落在开发设备和自己管理的密钥上，云盘与远端存储只负责保存字节。

| 做法 | 服务侧可能接触到什么 | 适用边界 |
|---|---|---|
| 直接推到 GitHub Private | 普通 Git 对象、源码与提交历史，由平台访问控制保护 | 不满足核心源码不明文上云的前提 |
| 本地 Git Central + Cryptomator 密文网盘 | 云端看到加密文件及部分大小、时间等元数据；解密在受信任客户端 | 适合异地备份，但不是原生在线 Git 协作 |
| 客户端加密整个 Git 远端再推送 | 目标是远端只存密文；具体保护范围取决于实现 | 候选方向，必须验证性能、Git 语义和恢复 |
| 断开连接的 7z 归档 | 离线介质保存加密历史快照 | 防灾层之一，不提供跨地域实时协作 |

这条边界也有代价：本机被恶意软件控制、密钥泄露、AI 工具获得解密后的工作区时，客户端加密帮不上忙。[Cryptomator 的安全说明](https://docs.cryptomator.org/security/security-target/)明确列出了本机恶意软件以及文件大小、时间等元数据限制。“不上云明文”是可以审计的设计约束，不是宣称代码绝对不会泄漏。

![核心源代码不明文上云的 Git 备份架构示意图：Windows 和 Mac 本地开发仓库通过 Windows Central 裸仓库同步，E 盘镜像分别进入 Cryptomator 密文云备份与 7z 离线归档。](/images/posts/git-backup-win-mac-bare-repo-disaster-recovery/zero-trust-git-backup.zh-cn.svg)

*架构示意：日常 Git 协作保留在可信本地，云端只存客户端加密后的备份，离线归档独立保存历史。加密 Git remote 和手机端开发属于后续设计方向。*

## Win/Mac 局域网 Git 同步：为什么用裸仓库做 Central

先看运行时的关系：

```text
Windows 开发仓库 ─── push / pull ──┐
                                  ├── Windows Central（bare repo）
Mac 开发仓库 ─ SMB ─ push / pull ──┘           │
                                             │ mirror 更新
                                             ▼
                                      E 盘镜像裸仓库
                                             │
                                 加密归档 / 离线复制
                                             ▼
                                    移动硬盘、异地网盘
```


四层里其实只有后三层才承担备份职责。把它们摊开看，哪一层会跟着出问题就清楚了：

| 层级与位置 | 关键动作 | 主要应对 | 不能解决的事 |
|---|---|---|---|
| 协作 Central：Windows D 盘裸仓库 | Win/Mac `push`、`pull` | 已推送代码在单台开发机损坏时仍可取回 | Windows 关机、SMB 断连、Central 本身损坏；它不算独立冷备 |
| 同机镜像：Windows E 盘 mirror | `clone --mirror`、`remote update` | Central 目录级损坏后从最近镜像恢复 | D/E 同机失效；`--prune` 可能同步删分支 |
| 离线归档：断开的移动硬盘 | 7z 加密包、`7z t` | 主机故障、勒索软件影响在线副本 | 更新靠人工；最新版本取决于归档时间 |
| 异地密文：Cryptomator + OneDrive / Google Drive | `robocopy`、密文清单校验、下载验证 | 本地设备全部丢失、异地取回 | 云同步也能传播错误；不含未提交代码及外部数据 |

RPO（最多损失到哪个版本）由各层**最后一次成功提交、同步或归档时间**决定；RTO（多久能恢复）还取决于介质速度、解密和演练情况。以前没有专门记录实际恢复耗时，所以这里只比较恢复路径，不用假定的分钟数冒充 RTO/RPO。D/E 同属一台 Windows，两个盘符也消除不了整机故障这个共同风险。

Central 的关键不是“有个共享文件夹”，而是里面存放的是一个 **bare repository（裸仓库）**。它有 Git 的对象、引用和仓库配置，却没有普通工作区。两台开发机都持有自己的工作树，修改在本机完成，提交以后才推到 Central。

按照 [Git 官方的 `git init` 文档](https://git-scm.com/docs/git-init)，新建裸仓库的基本形式是：

```bash
# 示例路径，不是我原机器的目录
git init --bare "D:/git-central/project.git"

# 已有 Windows 工作仓库中配置 Central
git remote add central "D:/git-central/project.git"
git push -u central master
```

Mac 侧先把 SMB 共享挂载到本地，再把挂载路径设为 `central` 的 remote。下面的 `master` 是当年仓库使用的分支名；如果你的默认分支是 `main`，不要照抄 `master`。

```bash
git remote add central "/Volumes/git-central/project.git"
git fetch central
git pull --ff-only central master
```


旧操作手册里还有几行非常具体的 Mac 端排查命令，省去了“究竟是 Git 没更新，还是 SMB 根本没挂上”的猜测：

```bash
# 在 Mac 上挂载 Windows 的 SMB 共享（示例地址）
open "smb://<windows-host>/<share-name>"

# 先确定共享真正挂载到了哪里
ls /Volumes
ls "/Volumes/<share-name>"

# 再从项目工作区确认 Git remote 指向的位置和当前分支
git remote -v
git branch --show-current
git status --short
```

原笔记记录的是先检查共享目录确实能看到裸仓库，再操作 `central` remote；不是看到 `git pull` 报错就急着 `reset --hard`。Windows 侧也遵循先看 `git status`、提交，再 `git push central master` 的顺序。这些排查命令保留自当年的操作笔记。

在原来的日常习惯里，开发前先 `git pull central master`，开发结束后看 `git status`，再 `git add .`、`git commit`、`git push central master`。另一台电脑从同一个入口拉取。示例中的 `--ff-only` 是我现在更愿意保留的保护：遇到分叉就停下来处理，避免普通 `pull` 默默产生合并提交。

这种安排有个很实在的好处：Central 坏掉时，两台开发机的 Git 历史还在；其中一台开发机坏掉，也不等于历史随它消失。我一直把“完整仓库可以作为重建的种子”当成设计原则。不过这句话有条件：那份仓库必须真的保有你需要的分支、标签和对象。只 clone 过单分支、做了浅克隆，或者把未提交文件放在工作树里的情况，不能自动算作完整备份。

还有一个容易忽略的边界：通过 SMB 直接访问裸仓库，实际依赖共享文件系统的权限、锁、网络稳定性。旧方案能满足自己的双机习惯，不代表适合跨地域、多用户或者不稳定网络。新建长期 Git 服务时，我会更愿意使用 Git over SSH 这样的远程传输，而不是把共享盘当成通用 Git 服务器。

## Git mirror 冷备：同步删除和历史快照是两回事

真正拉开安全距离的是 Central 后面那几层。原来的 E 盘放一份 `--mirror` 方式克隆的裸仓库，不参与开发，只做更新；移动硬盘留加密归档，平时离线保存；网盘再留一份经过加密处理的异地副本。它们不是四个相同的 `git clone`，而是应对不同故障范围。

Git 官方对 [`git clone --mirror`](https://git-scm.com/docs/git-clone) 的定义值得仔细看：mirror 不仅包含分支，还会按镜像 refspec 同步其他引用。它与普通开发克隆不同，后续 `git remote update` 可能覆盖对应 refs。初次创建和日常更新的示例可以写成：

```bash
# 只在初始化冷备仓库时执行一次
git clone --mirror "D:/git-central/project.git" "E:/git-cold/project.git"

# 后续更新镜像。--prune 可能删除源端已经不存在的引用
git --git-dir="E:/git-cold/project.git" remote update --prune

# 只检查 Git 对象，并不等于完成一次恢复演练
git --git-dir="E:/git-cold/project.git" fsck --full
```


当年的 E 盘冷备操作手册除了 `git remote update`，还明确写了一个确认当前提交的办法：

```bash
# 在 mirror 裸仓库内执行；本例沿用历史 master 分支
git remote update
git log origin/master -1 --oneline

# 不只比最近一条 log；正式恢复还需要核对 refs 和对象
git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags
```

这里 `origin/master` 是历史笔记中的命令写法；裸镜像能否以该名字访问引用，要以实际配置和 `git show-ref` 结果为准。更可靠的通用做法是直接查看 `refs/heads/master` 以及其他需要保存的 refs。

我用 Git 2.47.3 建了一个临时仓库，走了一遍 `--prune` 的删除链：推送 `test-prune` 分支、同步 mirror，然后从 Central 删除这个分支，再更新镜像。最关键的输出只有几行：

```text
git_version=git version 2.47.3
feature_after_sync=present 733c4dc75735672f2db8b4ed8185058ceddaf8c7
feature_after_prune=absent
git_fsck_exit=0
git_fsck_output=dangling commit 733c4dc75735672f2db8b4ed8185058ceddaf8c7
restored_master_sha=c7b763f667cbbba6c1bb39a6719236a554257a8c
```

注意看：`git fsck` 返回 0，还能找到 dangling commit，但镜像已经没有 `test-prune` 的分支引用。悬空对象还可能在后续垃圾回收时消失。镜像不是“时光机”——它会同步最新状态，也会同步删除。这个实验验证的是 Git 引用行为；7z 和网盘的可靠性仍需要用真实归档做恢复演练。

按照 [`git remote` 文档](https://git-scm.com/docs/git-remote)，`remote update` 做的是 fetch；`--prune` 会按配置清除已经失效的远端引用。每次更新通常只传输需要的新对象，不必把整个仓库重新拷一遍。但 **mirror 是同步副本，不是不可变快照**。上游误删了分支，下一次带 prune 的同步也可能把这种删除传播到镜像。它很适合快速恢复当前状态，却不能独自承担“防误删、防勒索”的所有责任。

所以当年还保留了两条不同的备份支线：一条把冷备仓库打成带日期的 7z 加密归档，放到平时断开的移动硬盘；另一条把 Git 镜像保存在 Cryptomator 的加密 vault，再将 vault 的**密文目录**复制到 OneDrive / Google Drive 同步目录。它们可能保护同一份 Git 历史，但不能简单画成“所有网盘副本都先经过 7z，再经过 Cryptomator”。真正有价值的是某一份历史归档在 Central 下一次更新时不会被覆盖。加密口令不能与归档放在一起，复制成功也不等于恢复成功。

同一台 Windows 上的 D 盘 Central 与 E 盘镜像，能缓解仓库误操作或单卷损坏，却挡不住整机故障、被盗或者同机勒索。移动硬盘只有在备份完成后真正断开，才有离线隔离的意义；云端存储如果同步了删除或损坏，也未必保留可用旧版。这样的差异，是我后来不再满足于“已经有好几份备份”的原因。

还要补一处范围：Git 保存已提交的 Git 对象和引用，并不会自动替你备份 `.gitignore` 排除的大数据、未提交的工作区变更、外部数据库。使用 Git LFS 或子模块的项目，也需要分别确认相应的对象和依赖是否已备份。仓库镜像的完整，不等于整个开发环境的完整。类似 Docker volume 这样的运行数据，要用另一套备份方案处理；我之前也单独写过 [Docker 数据卷备份到 GitHub 的容灾实践](/posts/docker-volume-backup-github-sync/)。

## Cryptomator + 网盘：SHA-256 如何校验加密备份

网盘这一层最麻烦的不是上传，而是上传之后如何确定没漏文件。当时我专门为 Cryptomator + OneDrive、Cryptomator + Google Drive 留了两份操作笔记，里面有密文基准清单、`VaultIntegrityCheck.ps1` 的调用顺序，以及复制完成后如何定位 `MISSING`、`CHANGED`、`EXTRA`。

Cryptomator 负责对文件内容、名称做客户端加密，网盘客户端负责传输，它们不是同一个软件的两项功能。 [Cryptomator 官方安全说明](https://docs.cryptomator.org/security/security-target/)也强调加密不能代替备份。实际旧流程更接近：

```text
E: 冷备镜像 / Cryptomator 加密 vault（基准）
     │
     ├─ 对稳定的密文目录生成指纹清单（Update）
     │
     ├─ robocopy 到 OneDrive 或 Google Drive 本地同步目录
     │
     ├─ 等网盘客户端显示同步完成
     │
     └─ 对目标密文目录只读比对（Verify）
          ├─ MISSING：源有、目标没有
          ├─ CHANGED：路径相同，但文件指纹变化
          ├─ EXTRA：目标多了文件，需判断冲突/临时文件
          └─ 匹配：本地同步目录与清单对应
```

原来的自用 `VaultIntegrityCheck.ps1` 固定了 `Update` 与 `Verify` 两种模式：**只在确认 E 盘密文源副本正确时更新基准**，验证目标目录时不刷新基准。下面是历史调用形式（路径为示例）；没有这个脚本，也可以用后面的原生 PowerShell 比较当前两个本地密文目录，但它不具备持久基准功能：

```powershell
# 只在确认 E: 源 vault 停止写入并且内容正确时更新基准
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "E:\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Update

# 等 OneDrive 上传完成，再对目标密文目录做只读校验
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "C:\CloudSync\OneDrive\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Verify
```

Google Drive 版本在两步中间还保留了一条 `robocopy` 复制命令，参数为 `/E /R:2 /W:2 /MT:8 /FFT /Z /NP`。换成中性目录后的可复制骨架是：

```powershell
robocopy "E:\BackupVault" "G:\MyDrive\BackupVault" /E /R:2 /W:2 /MT:8 /FFT /Z /NP
if ($LASTEXITCODE -ge 8) { throw "Robocopy failed with code $LASTEXITCODE" }
```

微软的 [Robocopy 退出码说明](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy/)确认：返回码 **8 及以上**才表示至少有一次复制失败。但 `0–7` 只是复制阶段没有报告这种失败，绝不能把它写成“所有文件 SHA-256 均通过”。`/E` 会包含子目录，但不负责删除目标已有的多余文件；我也不会在唯一云端副本上随手换成会删目标文件的 `/MIR`。

历史笔记留下的校验反馈有 `[OK] Vault matches the manifest exactly.`、`MISSING`、`CHANGED`、`EXTRA` 等类型。看到 `MISSING` 或 `CHANGED`，应先查上传是否完成、文件是否仍在写入，**不要对网盘目录运行 Update** 来把异常覆盖成新基准。Google Drive 笔记还记过 `.tmp.driveupload` 临时文件；它可能是同步中间状态，不能无条件忽略，等完成同步后重新验证更稳妥。

如果读者没有那份私人脚本，至少能先用 Windows 自带的 `Get-FileHash` 检查单个文件的 SHA-256。它是 PowerShell 的标准 cmdlet，算法和 `-LiteralPath` 参数见 [微软官方文档](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash)：

```powershell
Get-FileHash -Algorithm SHA256 -LiteralPath "E:\BackupVault\vault.cryptomator"
Get-FileHash -Algorithm SHA256 -LiteralPath "C:\CloudSync\OneDrive\BackupVault\vault.cryptomator"
```

但只查一个配置文件远远不够。整个 vault 的清单至少应覆盖**相对路径、文件大小和每个密文文件的 SHA-256**，并检测目标端多文件、少文件、同名变更；还要有明确的源基准日期，不能在两端一边写入一边比。没有现成校验工具，也可以用一段通用的只读 PowerShell 脚本处理：递归列出源、目标文件的相对路径、字节数和 SHA-256，再用 `Compare-Object` 找出差异：

```powershell
function Get-Inventory([string]$root) {
  $base = (Resolve-Path -LiteralPath $root -ErrorAction Stop).ProviderPath.TrimEnd([char]'\')
  Get-ChildItem -LiteralPath $base -File -Recurse -Force -ErrorAction Stop |
    ForEach-Object {
      [pscustomobject]@{
        Path   = $_.FullName.Substring($base.Length).TrimStart([char]'\')
        Bytes  = $_.Length
        SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
      }
    }
}
$original = @(Get-Inventory "E:\BackupVault")
$replica  = @(Get-Inventory "C:\CloudSync\OneDrive\BackupVault")
if (!$original.Count -or !$replica.Count) { throw "Empty or unavailable vault" }
$diff = Compare-Object $original $replica -Property Path,Bytes,SHA256
if ($diff) {
  $diff | Format-Table -AutoSize
  throw "Local encrypted vaults differ"
}
Write-Host "[OK] Local encrypted vault file hashes match"
```

这份新示例只覆盖文件，不覆盖空目录或云服务端状态，也没有历史 `Update/Verify` 的持久基准能力。真正的云端校验还差一步：**把云端某一版加密归档重新下载到与同步目录无关的位置**，再拿下载文件的哈希去比上传前留下的可信记录。只有“网盘同步图标已完成 + 本地目录一致”，并不能单独证明远端文件可取回。

对离线移动硬盘，我当年采用 7z 全量加密包而不是始终依赖热镜像。7z 的 `-mhe=on` 会加密 7z 格式内的文件名，`-p` 表示使用口令；不要把口令明文拼进共享的脚本、命令历史或日志。[7-Zip 官方 7z 格式说明](https://www.7-zip.org/7z.html)确认支持 AES-256。创建归档时，我更建议在 GUI 里确认口令和“加密文件名”；下面保留 CLI 参数示例，操作前先用本机 `7z -h` 核对版本和密码输入方式，不要把密码直接写进脚本或命令历史：

```powershell
# 只在冷备仓库没有写入时，对其目录创建带口令的 7z 归档
7z a -t7z -p -mhe=on "git-snapshot-YYYYMMDD.7z" "E:\GitCold\project.git"

# 测试归档是否能读完：输入口令后检查退出码
7z t "git-snapshot-YYYYMMDD.7z"
```

例如把这个归档从云端重新下载到 `D:\Downloads`，可以这样验证上传前后字节一致：

```powershell
$before = (Get-FileHash "D:\GitArchive\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
$after  = (Get-FileHash "D:\Downloads\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
if ($before -ne $after) { throw "Downloaded archive SHA-256 mismatch" }
```

哈希相同只能说明**这两份归档字节一致**，不证明基准本身未被篡改，也不能替代 `7z t`、解压、`git fsck`、refs 对比和实际克隆。基准清单最好独立保留并防止被同一故障一起篡改；移动硬盘备份完成后要真的断开。这样把“密文传输正确”和“代码历史可恢复”分成不同的检查，才不会在出事时把一个 `[OK]` 当成万能保险。



## 能否像 Git push 一样，把密文实时推到云端？

历史方案把两件容易混淆的事分开了：Windows / Mac 对**本地明文 bare Central** 做 Git push、Git pull；另一侧是把 Git mirror 的副本放进 Cryptomator vault，再将**加密后的文件目录**交给网盘同步。后者上传得再频繁，也只说明密文文件在同步；它不自动变成一个支持 Git 分支、fetch 和 push 的云端远程仓库，更不能据此说现有架构已经实现实时密文 Git push。

我还希望补上一条链路：本地每次 commit 后，尽可能及时地把**整份 Git 历史的密文版本**推到异地，服务侧读不到正常的源码与提交历史。真正需要解决的不是加一句“自动同步”，而是出站前加密、密钥和恢复凭证独立保管、push 失败与并发覆盖的处理，以及远程灾备的实际恢复验证。

可研究的工具之一是 [git-remote-gcrypt](https://github.com/spwhitton/git-remote-gcrypt)。它用 GnuPG 加密 Git remote 的表示，并可桥接 Git transport。但[项目文档](https://github.com/spwhitton/git-remote-gcrypt/blob/master/README.rst)警告：**使用普通 Git URL 后端时，push 会重复上传整个仓库历史，仓库增长后可能不再实用**；还记录了与非快进覆盖相关的危险默认行为。它的 rsync 后端能更好地进行增量传输，但这意味着使用自己控制的 rsync/SSH 存储端，而不是给普通 GitHub Private 配一个 remote 就能实现同等效果。这里的“及时推送”也只覆盖已提交的 Git 历史，不会自动同步未提交的工作区。在用于商业仓库前，还需要在隔离环境验证远端对象、增量成本、非快进保护、断线冲突、密钥轮换与恢复。

仅用 [git-crypt](https://github.com/AGWA/git-crypt/blob/master/README.md) 加密少数文件也不能满足同一目标：它不隐藏文件名、commit message 等 Git 元数据。更现实的备选方向，是继续**本地正常 Git 协作 + 自动生成密文异地增量备份**。这能缩短备份的滞后，却不能让 GitHub 网页直接显示 diff、搜索源文件或替云端 AI Agent 运行代码。我宁愿承认这是两种能力，也不为了手机访问方便，把原来的安全前提悄悄撤销。

## Git 灾难恢复：从哪份副本恢复代码和历史

原先给自己留了两套恢复路线。
遇到事故时，我会先判断手里**哪一份仓库还保有目标 refs**，而不是立刻重建一套空项目：

```text
需要恢复 Git 项目
  ├─ 开发机还有完整的提交 / 分支 / 标签
  │    └─ 新建空 bare Central → 检查 refs → 推送本地分支与 tags
  ├─ 开发机不完整，但 E 盘 mirror 仍能读取
  │    └─ fsck → 查看 refs → clone 出工作区，再比对历史
  └─ 在线副本都不可用
       └─ 找离线或异地历史归档 → 校验 SHA-256 → 7z 测试与解压
                                     → git fsck → refs 对照 → clone
```

这个判断也提醒了我：`git push --all` 只推送**本地**分支。不能因为当前工作区能启动，就默认过去所有开发机的全部分支都在这一份里。


一种是 Central 丢了，但还有完整的开发仓库。此时可以重新创建一个裸仓库，再从开发机推回需要的分支和标签：

```bash
# 在新 Central 的可写位置创建空裸仓库
git init --bare "/path/to/recovered/project.git"

# 在幸存的开发仓库内执行
git remote add recovered "/path/to/recovered/project.git"
git push recovered --all
git push recovered --tags
```

这里必须说明：`--all` 推送的是这台开发机的**本地分支**，不是所有 `refs/remotes/*`。如果另一台机器曾有独立分支，而这台开发机没有保存为本地分支，不能声称 `git push --all` 会替你凭空找回来。正式切换 Central 前，需要对照现存仓库的 refs 清单，必要时从其他幸存副本补齐。

另一种更极端：本地开发机和 Central 都不可用了。恢复的入口就变成移动硬盘或网盘里的归档。顺序应该是拿到一个明确日期的备份、完成解密和解压、检查裸仓库对象、核对预期 refs，最后 clone 出工作区。先不要把刚解出来的备份覆盖掉，更不要第一步就执行带 `--force` 或 `--prune` 的镜像推送。

```bash
# 在解压得到的裸仓库上执行
git --git-dir="/restore/project.git" fsck --full
git --git-dir="/restore/project.git" for-each-ref \
  --format='%(refname) %(objectname)' refs/heads refs/tags

# 确认仓库可读后，再建工作区
git clone "/restore/project.git" recovered-worktree
```

[`git fsck`](https://git-scm.com/docs/git-fsck) 检查 Git 对象的连通性与有效性；[`git for-each-ref`](https://git-scm.com/docs/git-for-each-ref) 能列出分支和标签对应的对象 ID。两者一起，至少能回答“包是不是损坏了”“预期的分支和标签在不在”。但要证明真的可恢复，还得检查文件内容、依赖、外部数据，以及是否能在新的机器上继续构建和工作。

后来整理 `verify-coldbackup.ps1` 时，我把检查顺序固定成一条链：**7z 完整性测试 → 解密解包 → clone 出工作区 → `git fsck` 检查对象 → 对照全部分支和标签 SHA**。只看“压缩包能打开”远远不够；要确认代码真的备住了，就得在独立目录完整走一次恢复。

顺带说一句，另一条“把上游公开项目镜像到自己 GitHub 私有仓”的路径，与这个局域网 Central 是两件事。那条路线要处理 GitHub 的受限 refs、分支覆盖和 `--force --prune` 的目标删除风险，不应该把个人开发分支混在镜像目标里。具体镜像问题我已经单独放在[GitHub 公开仓库自动同步到私有镜像的实践](/posts/github-public-repo-private-mirror-backup/)里，不在这里重复展开。官方 [`git push` 文档](https://git-scm.com/docs/git-push)对 `--mirror`、`--prune` 和 refspec 的差异也说得很清楚。


## 手机端 AI 开发遇到的瓶颈，恰好是这套架构的代价

过去在 Windows 和 Mac 前面工作，这个选择并不难忍受：两台机器都有完整代码，Windows Central 负责中转。现在有时只有手机，想临时让 ChatGPT/Codex 查看仓库、修个小问题或跑一次检查，限制就集中暴露出来了。

首先，Windows 是单点在线入口，关机、网络变化或 SMB 无法访问时，跨设备协作就停了。Mac 的完整仓库也占空间，但 partial clone、sparse checkout 只能缓解容量压力，解决不了谁有权解密的问题。更关键的是，**云端 AI 编程工作流通常需要直接读取、检索和执行源码**。如果云端仓库只有密文，普通 GitHub PR、网页 diff、代码搜索和云端 Agent 无法理解它；如果为了让 Agent 工作而在不受信任的服务器上解密，又回到我最初拒绝的明文上云。

所以下一步更符合这条边界的选择，不是把 GitHub Private 直接升级成唯一 Central，而是**让手机成为控制台，让代码继续留在我信任的开发机器上**：例如通过受控的私有网络和 SSH 连接一台始终开机的自有 Mac/Linux 主机，在那里运行 Git、测试和获得授权的开发工具。手机不持有完整仓库，也无需把密钥交给云 Git 服务。这条路仍依赖主机在线、强身份认证和最小权限。若云端模型需要接收完整源码上下文，仍须另行判断数据出站是否违反同一边界，不能因为执行主机在家中就默认安全。

即使未来建成独立的加密 Git remote，也只能解决远程传输和历史保存，**不能自动解决云端 Agent 读取明文的需求**。需要分别回答：谁能解密？明文在哪台设备运行？手机断线后任务能否安全继续？这几个问题没闭环之前，我不会把移动协作包装成已经完成的升级。

冷备与恢复演练仍然要留着。一个推送更方便的通道，不会自动变成防误删和可恢复的历史快照。

## Git 裸仓库、SMB 和备份恢复的几个常见问题

### 为什么不用 SMB 直接共享普通 Git 工作区？

普通工作仓库有工作树，Git 默认拒绝向当前检出的非裸仓库分支直接推送（[`receive.denyCurrentBranch`](https://git-scm.com/docs/git-config)）。裸仓库无工作树，适合做 Central；开发仍留在 Win/Mac 各自的工作区。

### Mac 挂载 SMB 后 `git pull` 失败，该先检查什么？

先核对 `ls /Volumes`、`git remote -v` 和共享权限，再确认是否仍有 Git 进程。锁文件可能出现在工作仓库或裸仓库的 refs 等位置；**未确认进程结束前，不要直接删除 `.lock`**。跨网络频繁操作可考虑 Git over SSH。

### `git clone --mirror` 比普通 `git clone` 更安全吗？

`--mirror` 会同步覆盖 refs，源端删掉的分支可能在下一次更新时从镜像消失。防误删靠**保留不随源端更新的历史归档**，不是再复制一份同样的镜像。

### OneDrive / Google Drive 显示“已同步”，就算通过哈希校验了吗？

不算。客户端“已同步”、本地密文目录与清单一致、云端重新下载后 SHA-256 一致，以及最终解密、`git fsck`、核对 refs、实际 clone，是不同检查。历史脚本的 `[OK]` 只覆盖它比对的本地目标与基准。

### 为什么不直接用 GitHub Private？恢复演练多久做一次？

GitHub Private 的权限保护不等于云端没有明文源码，不满足我的核心商业代码边界。首次配置或修改加密备份链后至少独立恢复一次，检查密钥、Git 对象与 refs，再实际 clone；**丢失密钥就无法解密备份**。

参考资料：[Git init](https://git-scm.com/docs/git-init) · [Git clone](https://git-scm.com/docs/git-clone) · [Git remote](https://git-scm.com/docs/git-remote) · [Git push](https://git-scm.com/docs/git-push) · [Git fsck](https://git-scm.com/docs/git-fsck) · [Robocopy](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy/) · [Get-FileHash](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash) · [Cryptomator 安全边界](https://docs.cryptomator.org/security/security-target/) · [7-Zip 官方 7z 格式](https://www.7-zip.org/7z.html) · [GitHub Private 仓库条款](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#e-private-repositories) · [git-remote-gcrypt](https://github.com/spwhitton/git-remote-gcrypt) · [git-crypt 限制](https://github.com/AGWA/git-crypt/blob/master/README.md)
