掌握基礎操作後,真正拉開差距的是在歷史中精準操作、在出事時冷靜救援的能力。本章涵蓋互動式 rebase、cherry-pick、reflog 救援、bisect 定位 bug、worktree、程式碼考古,以及 Git LFS。
一、互動式 Rebase#
git rebase -i 讓你在重新播放 commit 時,逐條決定如何處理——整理一條雜亂的 feature 分支歷史的主力工具。
# 整理最近 4 個 commit
git rebase -i HEAD~4編輯器中每行一個 commit,把 pick 改成對應動作:
| 動作 | 簡寫 | 作用 |
|---|---|---|
pick | p | 保留 |
reword | r | 保留變更,但改 commit 訊息 |
edit | e | 暫停以修改該 commit 的內容 |
squash | s | 併入前一個 commit,合併兩者訊息 |
fixup | f | 併入前一個 commit,丟棄本 commit 訊息 |
drop | d | 刪除該 commit |
pick a1b2c3d feat: add search endpoint
fixup b2c3d4e wip
reword c3d4e5f fix typo in handler
drop d4e5f6a debug print互動式 rebase 會改寫 commit hash。只在尚未 push 或自己獨占的分支上做;共用分支請改用 revert(見 ../01-git-fundamentals/)。
搭配
git commit --fixup=<hash>與git rebase -i --autosquash,可在開發中先標記「這個 commit 是修某個舊 commit 的」,rebase 時自動排好順序,省去手動編輯。
二、Cherry-pick#
git cherry-pick 把指定 commit 的變更套用到當前分支,常用於把修復「移植」到其他分支(如把 main 的 hotfix 帶回 release 分支)。
# 套用單一 commit
git cherry-pick <commit>
# 套用一段連續 commit(不含 start)
git cherry-pick <start>..<end>
# 只取變更不立刻提交,方便調整後再 commit
git cherry-pick -n <commit>cherry-pick 會產生新的 commit hash(內容相同但 parent 不同)。同一變更在兩條分支各有一份 commit 是正常的,呼應 GitLab Flow 的 upstream-first:先進主線,再 cherry-pick 到下游。
三、reflog 救援#
git reflog 記錄 HEAD 的每一次移動——即使某個 commit 已不被任何分支指向,只要還在 reflog 裡,就救得回來。它是大多數「以為丟了」場景的救命稻草。
# 查看 HEAD 的移動歷史
git reflog
# 輸出範例
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# d4e5f6a HEAD@{1}: commit: 重要的工作
# ...
# 回到 reset 之前的狀態
git reset --hard HEAD@{1}reflog 是本地的,不會推送到遠端,預設保留約 90 天。它記錄的是「HEAD 去過哪」,所以幾乎所有透過 commit 產生過的狀態都能找回——前提是別等到
git gc把無引用物件清掉。
四、不小心 reset --hard 如何救回#
這是最常見的「闖禍」場景。冷靜按流程走,多數情況可完全復原。
flowchart TD
A[執行了 reset --hard, 變更不見了] --> B{變更曾經<br/>commit 過嗎?}
B -->|是| C[git reflog 找到丟失的 commit]
C --> D[git reset --hard HEAD@n<br/>或 git cherry-pick]
B -->|否, 但曾 git add| E[git fsck --lost-found<br/>找 dangling blob]
E --> F[從 blob 還原檔案內容]
B -->|否, 從未 add 過| G[Git 無紀錄<br/>嘗試 IDE local history 或編輯器復原]# 情況一:丟失的是 commit
git reflog # 找到目標 HEAD@{n}
git reset --hard HEAD@{n} # 或 git branch rescue HEAD@{n}
# 情況二:只 add 過、未 commit
git fsck --lost-found # 列出 dangling blob
git cat-file -p <blob-hash> # 檢視內容,手動還原從未
git add過的變更,Git 完全沒有紀錄,無法救回。這也是「頻繁 add/commit」的另一個理由——它讓 reflog 與物件庫成為你的安全網。
五、bisect 定位引入 bug 的 commit#
git bisect 用二分搜尋在大量 commit 中快速定位「哪一個 commit 引入了 bug」,N 個 commit 只需約 log₂N 次測試。
git bisect start
git bisect bad # 當前是壞的
git bisect good <old-commit> # 某個已知正常的舊 commit
# Git 自動 checkout 到中點,你測試後回報:
git bisect good # 這個版本正常
git bisect bad # 這個版本有問題
# ... 重複,直到 Git 指出第一個壞掉的 commit
git bisect reset # 結束,回到原本位置若有可自動判定好壞的腳本(回傳 0 為 good、非 0 為 bad),可全自動跑完:
git bisect run ./test-script.sh
六、worktree 多分支並行#
git worktree 讓同一個倉庫同時 checkout 多個分支到不同目錄,不必反覆 stash/切換——例如一邊開發功能,一邊在另一個目錄緊急修 bug。
# 為 hotfix 分支建立獨立工作目錄
git worktree add ../project-hotfix hotfix/urgent
# 查看所有 worktree
git worktree list
# 用完移除
git worktree remove ../project-hotfix各 worktree 共用同一份
.git物件庫,但有獨立的工作區與 HEAD。同一個分支不能同時被兩個 worktree checkout,避免狀態衝突。
七、程式碼考古:git log -S 與 git blame#
當你需要回答「這行詭異的程式碼是誰、為什麼加的」「某個函式什麼時候消失的」:
# 逐行追溯:每行最後由哪個 commit 改動
git blame <file>
# 找出「新增或刪除某段字串」的 commit(pickaxe)
git log -S "someFunctionName" -- <file>
# 找出符合正則的變更
git log -G "regex-pattern" -- <file>
# 看某個 commit 的完整脈絡(找到後)
git show <commit>
git log -S回答「這段程式碼何時被引入或刪除」,git blame回答「現在每行屬於哪個 commit」。兩者搭配,能順藤摸瓜找到當初的 MR 與決策紀錄(見 ../04-merge-request-workflow/)——這正是把版本歷史當作活文件的考古起點。
八、Git LFS 簡述#
Git 不擅長處理大型二進位檔(影音、模型、設計稿):每次修改都整份存一次 blob,倉庫會迅速膨脹。Git LFS(Large File Storage) 把大檔內容存到外部儲存,倉庫裡只留一個指標。
git lfs install
git lfs track "*.psd" # 指定哪些檔案走 LFS
git add .gitattributes # track 規則記錄在這裡
git add design.psd
git commit -m "chore: add design source"LFS 解決的是大型二進位檔。一般原始碼、設定、文件不需要、也不該走 LFS。導入前先評估遠端平台的 LFS 儲存配額與成本。