# 如何把公開 GitHub 儲存庫完整鏡像備份到自己的 Private 儲存庫

> Canonical: https://vxi.me/zh-tw/posts/github-public-repo-private-mirror-backup/
> Locale: zh-tw
> Published: 2026-10-05
> Updated: 2026-10-05
> Markdown: https://vxi.me/zh-tw/posts/github-public-repo-private-mirror-backup.md

如果你想長期儲存一個公開 GitHub 儲存庫，又不想依賴上游儲存庫一直存在，最直接的辦法之一就是：把它完整鏡像到自己帳號下的 Private 儲存庫。

這裡說的“鏡像”不是普通 `git clone`，而是用 `git clone --mirror` + `git push --mirror` 同步 Git refs。它適合做原始碼級備份，但**不等於備份整個 GitHub 專案**：Issues、Pull Requests、Release 附件、儲存庫設定、Secrets 等平臺資料不會一起複製；如果儲存庫用了 Git LFS，也需要額外同步 LFS 物件。

> **注意：** `git push --mirror` 會讓目標儲存庫的 refs 與本地鏡像保持一致，可能強制更新或刪除目標端多餘 refs。目標儲存庫最好專門用於鏡像備份，不要和日常開發儲存庫混用。

## 什麼時候適合用鏡像備份

這種方式比較適合下面幾種場景：

- 依賴某個公開開源儲存庫，希望自己保留一份完整 Git 歷史。
- 擔心上游儲存庫刪除、改名或轉為不可存取。
- 想定期同步上游分支和標籤，但不需要透過 Fork 參與協作。
- 希望備份儲存庫保持 Private，只作為自己的還原副本。

如果你只是想改程式碼並向上遊提交 PR，Fork 更合適；如果只是臨時讀程式碼，普通 clone 就夠了。

## 鏡像會備份什麼，不會備份什麼

`git clone --mirror` 會建立 bare mirror，並設定成跟蹤遠端 refs。配合 `git push --mirror`，可以把本地鏡像中的 refs 同步到另一個 Git 儲存庫。

它主要覆蓋：

- Git 提交歷史
- 分支 refs
- 標籤 refs
- 其他遠端可見 Git refs
- Git 物件

它**不會**自動複製 GitHub 平臺層的資料，例如：

- Issues
- Pull Requests
- Discussions
- Release 附件
- Actions Secrets / Variables
- Webhook
- Collaborators / 權限
- Branch protection / Rulesets
- 儲存庫可見性設定

另外，Git LFS 檔案在 Git 歷史中主要儲存的是指標，因此 LFS 物件要單獨遷移。

## 前置條件

本機先準備：

- Git
- GitHub CLI（`gh`）
- 如果源儲存庫使用 Git LFS，再安裝 Git LFS

確認 GitHub CLI 已經登入：

```bash
gh auth status
gh auth setup-git
```

如果尚未登入：

```bash
gh auth login
```

不要把 PAT、密碼或其他憑據直接寫進儲存庫 URL、腳本或檔案裡。優先使用 GitHub CLI 的憑據設定，或者 SSH。

## 第一次建立 Private 鏡像儲存庫

假設你要備份：

```text
https://github.com/OWNER/REPOSITORY
```

可以在準備存放本地鏡像的目錄裡執行：

```bash
set -euo pipefail

UPSTREAM_URL="https://github.com/OWNER/REPOSITORY.git"
REPO_NAME="REPOSITORY"
ACCOUNT="$(gh api user --jq .login)"
BACKUP_REPO="$ACCOUNT/$REPO_NAME"
BACKUP_URL="https://github.com/$BACKUP_REPO.git"
MIRROR_DIR="$REPO_NAME.git"

if gh repo view "$BACKUP_REPO" >/dev/null 2>&1; then
  echo "Target repository already exists: $BACKUP_REPO"
  exit 1
fi

if [ -e "$MIRROR_DIR" ]; then
  echo "Local path already exists: $MIRROR_DIR"
  exit 1
fi

gh repo create "$BACKUP_REPO" --private

git clone --mirror "$UPSTREAM_URL" "$MIRROR_DIR"

git -C "$MIRROR_DIR" remote add backup "$BACKUP_URL"

git -C "$MIRROR_DIR" push --mirror backup
```

這裡有兩個安全點值得保留：

1. **目標儲存庫已存在時直接停止。** 不要預設覆蓋。
2. **本地鏡像目錄已存在時也停止。** 先確認它是不是對應同一個上游。

另外，儲存庫的 Private 屬性由 GitHub 儲存庫設定決定，Git push 本身不會改變儲存庫可見性。

## 為什麼不是普通 clone + push

普通工作區 clone 更偏向日常開發，主要圍繞當前分支、遠端跟蹤分支和工作區檔案。

鏡像備份的目標不同：我們希望保留遠端 refs，並讓另一個儲存庫儘量和上游 Git 狀態一致。

```bash
git clone --mirror "$UPSTREAM_URL" "$MIRROR_DIR"
```

建立的目錄本身就是 bare repository，沒有普通工作區。對於專門做同步和備份的任務，這反而更乾淨。

## 後續怎麼增量更新

第一次鏡像完成後，不需要每次重新 clone。

進入包含 `REPOSITORY.git` 的父目錄，執行：

```bash
git -C REPOSITORY.git fetch --prune origin
git -C REPOSITORY.git push --mirror backup
```

其中：

- `origin`：公開上游儲存庫
- `backup`：你自己的 Private 鏡像儲存庫

`fetch --prune origin` 會拉取上游最新 refs，並清理已經從上游消失的遠端 refs；隨後 `push --mirror backup` 再把當前鏡像狀態同步到目標端。

如果你把鏡像統一放在固定目錄，也可以直接使用絕對路徑：

```bash
git -C "$HOME/git-mirrors/REPOSITORY.git" fetch --prune origin
git -C "$HOME/git-mirrors/REPOSITORY.git" push --mirror backup
```

## Git LFS 儲存庫要額外處理

如果專案用了 Git LFS，只同步 Git refs 不夠。

先確認：

```bash
git lfs version
```

在鏡像目錄中同步 LFS 物件：

```bash
git -C REPOSITORY.git lfs fetch --all origin
git -C REPOSITORY.git lfs push --all backup
```

大儲存庫執行 `--all` 可能需要比較長時間，也會消耗目標儲存庫的 LFS 儲存和流量配額。

對於後續更新，可以使用：

```bash
git -C REPOSITORY.git fetch --prune origin
git -C REPOSITORY.git lfs fetch --all origin
git -C REPOSITORY.git lfs push --all backup
git -C REPOSITORY.git push --mirror backup
```

這樣做的好處是，在更新目標 refs 前，先儘量確保對應 LFS 物件已經同步過去。

## 怎麼確認備份真的成功

第一步先確認目標儲存庫確實是 Private：

```bash
ACCOUNT="$(gh api user --jq .login)"

gh repo view "$ACCOUNT/REPOSITORY"   --json nameWithOwner,visibility,defaultBranchRef
```

然後比較上游和備份儲存庫當前公開的 refs 與物件 ID：

```bash
UPSTREAM_URL="https://github.com/OWNER/REPOSITORY.git"
ACCOUNT="$(gh api user --jq .login)"
BACKUP_URL="https://github.com/$ACCOUNT/REPOSITORY.git"

git ls-remote --refs "$UPSTREAM_URL"   | awk '{print $1, $2}'   | sort > /tmp/upstream-refs.txt

git ls-remote --refs "$BACKUP_URL"   | awk '{print $1, $2}'   | sort > /tmp/backup-refs.txt

diff -u /tmp/upstream-refs.txt /tmp/backup-refs.txt
```

如果 `diff` 沒有輸出，並且退出碼是 `0`，說明兩邊當前列出的 refs 和物件 ID 一致。

對於 LFS 專案，還應該從 Private 儲存庫實際 clone 或 checkout 一次，確認大檔案可以正常下載，而不是隻看 Git refs。

## `push --mirror` 最大的坑

最需要記住的一點就是：

> Mirror 是“對齊”，不是“追加備份”。

如果目標儲存庫中存在上游沒有的 refs，`git push --mirror` 可能會把它們刪除；如果同名 refs 指向不同物件，也可能被強制更新。

因此最穩妥的原則是：

- 一個上游儲存庫對應一個專用 Private 鏡像儲存庫。
- 不要在鏡像儲存庫裡額外開發自己的分支。
- 不要把鏡像儲存庫當成普通協作儲存庫。
- 第一次同步前檢查目標是否為空。
- 自動化前先手工跑通一次完整流程。

## 如果要自動化，我會怎麼做

長期儲存多個公開儲存庫時，可以把更新動作放進計劃任務或 CI：

```bash
git -C "$MIRROR_DIR" fetch --prune origin

if git -C "$MIRROR_DIR" lfs env >/dev/null 2>&1; then
  git -C "$MIRROR_DIR" lfs fetch --all origin
  git -C "$MIRROR_DIR" lfs push --all backup
fi

git -C "$MIRROR_DIR" push --mirror backup
```

不過自動化之前，最好再加三層保護：

- 明確固定允許操作的上游 URL。
- 明確固定允許推送的備份儲存庫。
- 目標儲存庫不是預期儲存庫時直接退出。

因為一旦把 `push --mirror` 放進無人值守任務，最重要的就不是“少寫幾行腳本”，而是避免它推錯目標。

## 讓 CLI AI 幫你執行時，提示詞要寫清楚邊界

如果你習慣讓 CLI Agent 或編碼助手代執行，可以給它類似這樣的任務：

```text
请把我提供的公开 GitHub 仓库完整镜像到当前 GitHub 登录账号下的同名 Private 仓库。

执行要求：
1. 先检查 git、gh 和 gh auth status。
2. 从公开仓库 URL 解析仓库名。
3. 创建目标前检查同名仓库是否已经存在；若已存在，停止并汇报，不覆盖。
4. 检查本地 mirror 目录是否已存在；若目录用途不明确，停止，不删除。
5. 创建空 Private 仓库，不创建 Fork。
6. 使用 git clone --mirror 克隆上游。
7. 添加 backup remote，并执行 git push --mirror backup。
8. 如果仓库使用 Git LFS，同步 LFS 对象。
9. 最后确认目标仓库 visibility=PRIVATE，并比较两边 git ls-remote --refs。
10. 给出以后增量更新使用的 fetch --prune + push --mirror 命令。

安全边界：
- 不打印或索取 Token、密码、Cookie、私钥。
- 不修改公开源仓库。
- 不删除任何已有仓库或未知本地目录。
- 目标仓库已有内容时必须停止。
- 不执行生产部署或其他无关操作。
```

對 AI Agent 來說，最重要的不是告訴它“把儲存庫備份一下”，而是明確哪些情況必須停下來。

## 這套方案的邊界

最後再總結一次：這是一套 **Git 儲存庫鏡像備份**，不是 GitHub 全量災備。

如果你需要真正完整地儲存一個 GitHub 專案，還要另外考慮：

- Issues / Pull Requests 匯出
- Releases 和附件
- Wiki
- Actions 設定與變數
- 儲存庫權限和規則
- Packages / Container images
- Discussions

但如果你的主要目標是“即使公開儲存庫以後消失，我仍然保留完整原始碼歷史、分支和標籤”，那麼 `clone --mirror + push --mirror` 是一條簡單、透明、可驗證的路徑。

## 官方參考

- [GitHub Docs：Duplicating a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/duplicating-a-repository)
- [GitHub CLI：gh repo create](https://cli.github.com/manual/gh_repo_create)
- [Git：git push](https://git-scm.com/docs/git-push)
- [Git LFS：git lfs fetch](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-fetch.adoc)
- [Git LFS：git lfs push](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-push.adoc)
