# git checkout --orphan:開一條「沒有歷史」的分支
## 什麼時候會需要
幾個常見的情境:
- 專案累積了幾年的 commit,裡面混著不該公開的東西,想「從現在這個狀態重新開始」,但又不想丟掉現有檔案。
- 想在同一個 repo 放一份完全獨立的內容,例如網站靜態檔(`gh-pages`)、文件、部署產物,不想讓它跟程式碼共用歷史。
- 想建立一個乾淨的第一個 commit,作為新的起點。
這幾件事共通的需求是:**新分支的第一個 commit 不要有父代。**
## 先建立畫面:commit 是一條鏈,每一環都記得上一環
把 git 的歷史想成一條鏈子,每個 commit 都記著「我的上一個 commit 是誰」(parent)。
```
c1 ← c2 ← c3 master
```
一般的 `git checkout -b feature` 是在鏈子的某一環上再接一條新分支,新分支的歷史**繼承**了 master 的整條鏈。
`--orphan` 做的事不一樣:它讓 `HEAD` 指向一個**還不存在**的分支名稱。因為這個分支還沒有任何 commit,你接下來做的第一個 commit,就找不到「上一環」可以記,於是成為一條新鏈的起點(root commit)。
```
c1 ← c2 master
(兩條鏈互不相連)
x 新分支的第一個 commit,沒有 parent
```
## 指令
```bash
git checkout --orphan <新分支名稱>
```
新版 git 也可以用 `git switch --orphan <新分支名稱>`,行為略有不同,見後面「實驗 4」。
## 實驗:實際看一次發生什麼
以下都在一個暫存 repo 裡實際執行(git 2.54.0)。起點是 `master` 上有兩個 commit,另外有一個沒被追蹤的檔案 `untracked.log`:
```
b769e99 c2 add b
8caf476 c1 add a
```
### 實驗 1:下指令之後,狀態長什麼樣
```bash
git checkout --orphan fresh
```
接著依序檢查:
```
--- branch 清單
master ← 看不到 fresh
--- HEAD 指向
refs/heads/fresh ← 但 HEAD 已經指向它了
--- status
A a.txt ← 原本的檔案全部變成「待新增」
A b.txt
?? untracked.log
--- log
致命錯誤: 您的目前分支 'fresh' 尚無任何提交
--- master 還在嗎
b769e99 ← 完全沒動
```
三個重點:
1. **分支名稱此時只存在於 `HEAD`,分支清單看不到**,因為分支本質上是「指向某個 commit 的名字」,沒有 commit 就沒有東西可以指。
2. **檔案都還在**。git 沒有動你的工作目錄,只是把「現在的內容」全部當成要放進第一個 commit 的新檔案(暫存區裡全是 `A`)。
3. **舊分支完全沒被碰**。建立 orphan 只是新增,不會刪任何東西。
### 實驗 2:做第一個 commit
```bash
git commit -m "fresh root"
```
```
* fresh e640695 fresh root
master b769e99 c2 add b
```
現在分支出現了。檢查這個 commit 是不是真的沒有爸爸:
```
$ git cat-file -p fresh
tree 6ffe151c467ccc36a00dacb41acd215d7a0179ed
author lab <lab@example.com> ...
committer lab <lab@example.com> ...
```
一般的 commit 在 `tree` 之後會有一行 `parent <hash>`,這裡沒有。再確認兩條歷史完全沒有共同祖先:
```
$ git rev-list --max-parents=0 fresh # 列出所有 root commit
e640695
$ git merge-base master fresh # 找共同祖先
(沒有輸出,exit code = 1)
```
### 實驗 3:兩條不相干的歷史能合併嗎
```
$ git checkout master && git merge fresh
致命錯誤: 拒絕合併無關的歷史
```
git 預設不讓兩條沒有共同祖先的歷史合併,因為這通常代表你合錯分支了。真的要合,必須明講:
```bash
git merge --allow-unrelated-histories fresh
```
```
* 1b03b45 merge unrelated
|\
| * e640695 fresh root
* b769e99 c2 add b
* 8caf476 c1 add a
```
合併之後兩條鏈才接在一起,但 `fresh root` 本身仍然沒有 parent。
### 實驗 4:想從空白開始,而不是帶著舊檔案
`--orphan` 預設把舊檔案留下來。要清空,補一個 `git rm -rf .`:
```bash
git checkout --orphan clean
git rm -rf .
```
```
--- 工作目錄剩什麼
untracked.log ← 沒被追蹤的檔案不會被清掉
--- status
?? untracked.log
```
`git rm` 只處理「被 git 追蹤的檔案」。**沒被追蹤的檔案(包含被 `.gitignore` 擋掉的,例如 `node_modules`、build 輸出)會留在原地**,不會隨著清空消失。要連它們一起清,得另外處理,而這是會刪掉東西的動作,下手前先確認。
清空之後如果想先留一個空的起點:
```bash
git commit --allow-empty -m "empty root"
```
這個 commit 裡的檔案數是 0(`git ls-tree -r HEAD | wc -l` 得到 `0`)。
`git switch --orphan` 則是直接幫你做了「清空被追蹤檔案」這一步:
```bash
git switch --orphan clean2
```
```
--- 工作目錄剩什麼
untracked.log ← 被追蹤的檔案已清掉,沒被追蹤的仍在
```
| | 被追蹤的舊檔案 | 沒被追蹤的檔案 |
|---|---|---|
| `git checkout --orphan` | 保留(放在暫存區) | 保留 |
| `git switch --orphan` | 清掉 | 保留 |
### 實驗 5:只開分支、沒 commit 就走人
```bash
git switch master
git branch --list 'clean2' # 空的
```
分支沒有 commit 就等於不存在,離開之後它就消失了,沒有任何殘留。
## 常見做法:用 orphan 重建 master,丟掉舊歷史
```bash
git checkout --orphan newroot
git add -A
git commit -m "squashed start"
git branch -D master # 刪掉舊的 master
git branch -m master # 把 newroot 改名成 master
```
實際執行的結果:
```
已刪除分支 master(曾為 b769e99)。 ← git 會把舊的 hash 印出來
6f23096 squashed start ← 現在的 master 只有一個 commit
```
**舊歷史真的丟了嗎?** 沒有那麼快。分支被刪掉,只是「指向 commit 的名字」不見了,commit 本身還在 repo 裡:
```
$ git cat-file -t b769e99
commit ← 物件還在
$ git reflog | grep b769e99
b769e99 HEAD@{3}: checkout: ... ← reflog 找得到
$ git branch rescued b769e99 ← 重新替它取名字就救回來了
$ git log --oneline rescued
b769e99 c2 add b
8caf476 c1 add a
```
所以風險分兩層:
- **本機**:刪掉分支後,在 git 清理(gc)過期之前,可以用 `reflog` 或刪除時印出的 hash 救回。
- **遠端**:如果接著 `git push --force` 覆蓋遠端的 master,別人的複本、CI、PR 都還指著舊歷史,團隊要重新 clone 或 reset。這部分我沒有在實驗裡驗證,但影響面是整個團隊,動手前務必先確認。
## 成立條件與例外
- 實驗用的是 git 2.54.0。`--orphan` 在 `git checkout` 與 `git switch` 上都有,但 `git switch --orphan` 需要 git 2.23 以上。
- 「檔案會留在暫存區」是 `git checkout --orphan` 的行為;如果你的 git 版本或別名行為不同,先用 `git status` 看一眼再繼續。
- 本筆記的實驗只涵蓋本機 repo;與遠端(`push`、保護分支、PR)互動的情況沒有實驗。
## 速查
| 想做的事 | 指令 |
|---|---|
| 開一條沒有歷史、但保留現有檔案的分支 | `git checkout --orphan <名稱>` 然後 `git commit` |
| 開一條完全空白的分支 | `git checkout --orphan <名稱>` → `git rm -rf .` → `git commit --allow-empty` |
| 同上,但一步清掉被追蹤的檔案 | `git switch --orphan <名稱>` |
| 確認某分支是不是獨立歷史 | `git merge-base <分支A> <分支B>`(沒輸出=沒有共同祖先) |
| 合併兩條獨立歷史 | `git merge --allow-unrelated-histories <分支>` |
| 看某分支的起點 commit | `git rev-list --max-parents=0 <分支>` |
| 救回被刪掉的分支 | `git branch <名稱> <刪除時印出的 hash>`,或從 `git reflog` 找 |