# 不把核心程式碼明文交給 GitHub：我的 Win/Mac 私有 Git、加密雲端備份與行動開發困境

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

當初搭建 Windows / Mac Git 系統，最重要的不是省錢，也不是避開網際網路，而是早就定下的底線：**長期投入、具有商業價值的核心原始碼，不以明文放到任何第三方雲端平台。** 就算 GitHub 倉庫標成 Private，我也不會把平台永遠不會越權、被入侵或遭遇供應鏈事件當成唯一保障。

這不是說 GitHub 一定會私自取得我的程式碼。Private 提供的是存取權限控制，**不等於營運平台在技術上完全無法讀取 Git 物件**。我想要的是自己保有金鑰，雲端只負責儲存密文；即便雲端帳號出事，也不能僅靠它就得到完整原始碼。

因此，我用 Windows bare repo 當本地 Central，Mac 透過 SMB 與它同步，後來才加上 E 碟 mirror、斷開連線的 7z 歸檔，以及 Cryptomator 加密後的 OneDrive / Google Drive 異地副本。**密文雲端備份不等於支援 push/pull 的加密 Git 遠端倉庫**，這個差別在我開始想用手機讓 ChatGPT/Codex 協助開發後，成了整套架構最大的痛點。

以下保留 2024–2026 年的操作筆記，以及 Git 2.47.3 小型合成倉庫的實際測試。原有測試只驗證 Git refs、mirror 與還原語義，不能當作密文即時 Git 推送或手機遠端開發已完成的證據。

## 為什麼我沒有直接選擇 GitHub Private？

我的威脅模型關注的是第三方雲端平台的信任邊界，不是指控平台會濫用資料。GitHub 的 [Private Repository 條款](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#e-private-repositories)提供保密與存取規則，但不等同端到端、由使用者獨立持有金鑰的整倉加密。對一般開發者來說 Private 可能已足夠；對我這類長期高價值核心程式碼，設計起點不同。

| 路線 | 雲端可能看見的內容 | 對我的用途 |
|---|---|---|
| 一般 GitHub Private | Git 物件、原始碼與提交歷史，由平台權限保護 | 不符合「核心程式碼不明文上雲」 |
| 本地 Git + Cryptomator 密文同步 | 加密檔案與部分大小、時間等中繼資料 | 可靠的異地備份方向，但不是 Git 網頁協作 |
| 用戶端先加密整個 Git remote | 目標是讓遠端只接收密文；細節依實作而定 | 可研究的遠端 push/pull 方向，尚未驗證 |
| 離線 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-tw.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]` 當成萬能保險。


## 我還需要的是能 push 的密文 Git，而不只是雲端同步

原先的日常 `git push`、`git pull` 是 Windows / Mac 對本地明文 bare Central 進行；Cryptomator 則把 mirror 副本轉成密文檔案，再交給 OneDrive / Google Drive 傳輸。兩個功能都很重要，但後者沒有提供 Git 網頁介面、遠端分支協作，不能寫成「我已經即時把加密 Git 物件推到 GitHub」。

要讓提交後的程式碼儘快以密文送到異地，必須在離開可信設備前加密、確認金鑰能單獨恢復、測試斷線重試與並發推送，而且不能把強制覆寫當作普通成功。這只處理已提交的 Git 歷史，不包含未提交工作區。

值得研究的是 [git-remote-gcrypt](https://github.com/spwhitton/git-remote-gcrypt)：它透過 GnuPG 表示加密的遠端 Git 倉庫，也能掛接標準 Git 傳輸。問題是[官方說明](https://github.com/spwhitton/git-remote-gcrypt/blob/master/README.rst)指出，使用一般 Git URL（例如雲端 Git 平台）時，每次 push 需要重傳整份歷史；而且有非快進保護不足的已知問題。相對地，rsync 傳輸方向支援增量，卻不是一般 GitHub Private 倉庫的使用方式。這些都需要先用隔離倉庫實測，不能當成已部署成果。

單靠 [git-crypt](https://github.com/AGWA/git-crypt/blob/master/README.md) 加密部分檔案也不等價：檔名、提交訊息和部分 Git 中繼資料仍可見。另一條路是維持本地 Git 協作，讓加密的異地備份自動、頻繁執行；這能改善備份延遲，但仍無法讓 GitHub 的 PR、diff 或雲端 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 公開倉庫自動同步至 Private 鏡像的實踐](/posts/github-public-repo-private-mirror-backup/)裡，不在這裡重複展開。官方 [`git push` 文件](https://git-scm.com/docs/git-push)對 `--mirror`、`--prune` 和 refspec 的差異也說得很清楚。

## 手機開發卡住的地方，正是保留明文邊界的代價

以前 Windows 和 Mac 都持有完整程式碼，我只要在本地完成提交就好。現在只有手機時，有時需要 ChatGPT/Codex 幫忙檢視某段程式碼、修改問題或跑測試；偏偏 Windows 是唯一 Central，關機或網路不可用就無法跨設備操作。

問題不只是 Mac 的本地儲存空間。partial clone、sparse checkout 能省空間，不能決定誰能解密。一般雲端 AI 開發服務必須讀取原始碼才能分析、執行測試或產生 PR；倘若把完整明文放進雲端工作區，就違反我最初的架構原則。

我更傾向讓**手機當控制台，明文仍留在可信的自有 Mac/Linux 主機**：透過受控的私人網路與 SSH 連線，在受信任設備上執行 Git 和開發工具。這仍是待驗證的方向，不是已經完成的手機端生產系統；必須考慮設備持續在線、金鑰、強認證和權限隔離。如果把原始碼內容傳給遠端 AI 模型，仍可能構成另一條明文出站路徑，不可假裝完全離線。

就算未來能從加密 Git remote 抓回密文歷史，雲端 Agent 也不會因此自動看懂它。我需要分別回答：誰能解密、明文在哪裡執行、手機離線後任務怎麼繼續。**密文遠端同步、手機操作與災難復原是三個不同問題。**

## Git 裸倉庫、SMB 和備份復原的幾個常見問題

### 為什麼不用 SMB 直接共享普通 Git 工作區？

一般工作倉庫有工作樹，Git 預設拒絕向非裸倉庫中目前檢出的分支直接 push（[`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)
