分支與合併
什麼是分支?
💡 比喻:平行宇宙 想像你在寫一篇小說,突然想試試「主角變壞人」的劇情, 但又不想改壞原本的故事。 分支就像開啟一個平行宇宙—— 你可以在新宇宙裡隨便改, 改得好就合併回主宇宙,改得爛就丟掉,完全不影響原本的故事。
主線(main) ──●──●──●──●──●──●──
\ ↗
功能分支(feature) ●──●──●
在平行宇宙開發新功能
分支基本操作
建立與切換分支
# 查看所有分支(* 標記目前所在分支)
git branch # 列出本地所有分支
# 建立新分支
git branch feature/login # 建立一個叫 feature/login 的新分支
# 切換到新分支(傳統方式)
git checkout feature/login # 切換到 feature/login 分支
# 切換到新分支(新語法,推薦)
git switch feature/login # 切換到 feature/login 分支(更直覺)
# 建立並同時切換(傳統方式)
git checkout -b feature/signup # 建立 feature/signup 並立即切換過去
# 建立並同時切換(新語法,推薦)
git switch -c feature/signup # 建立 feature/signup 並立即切換過去
# 查看所有分支(包含遠端)
git branch -a # 列出本地和遠端的所有分支
# 刪除已合併的分支
git branch -d feature/login # 刪除已合併的分支(安全刪除)
# 強制刪除分支(未合併也刪)
git branch -D feature/abandoned # 強制刪除分支(即使未合併)
# 重新命名分支
git branch -m old-name new-name # 把分支 old-name 改名為 new-name
分支命名慣例
常用的分支命名規則:
┌───────────────────────────────────────────────────┐
│ 類型 │ 命名格式 │ 範例 │
├───────────────────────────────────────────────────┤
│ 功能開發 │ feature/功能名稱 │ feature/login │
│ Bug 修復 │ bugfix/問題描述 │ bugfix/login-crash │
│ 緊急修復 │ hotfix/問題描述 │ hotfix/security-patch │
│ 版本發佈 │ release/版本號 │ release/v1.2.0 │
│ 實驗性功能 │ experiment/描述 │ experiment/new-ui │
└───────────────────────────────────────────────────┘
Merge 合併
💡 比喻:樹枝嫁接 Merge 就像把一根樹枝嫁接回主幹, 兩邊的「生長紀錄」都會保留下來, 歷史中可以清楚看到分支從哪裡分出、又在哪裡合併。
基本合併操作
# 步驟 1:先切換到要合併「進去」的目標分支
git switch main # 切換到 main 分支
# 步驟 2:執行合併
git merge feature/login # 把 feature/login 的變更合併進 main
# 合併後刪除分支(保持整潔)
git branch -d feature/login # 刪除已合併的 feature/login 分支
合併的兩種情況
Fast-forward 合併(快進合併):
main 沒有新的 commit,直接「快進」到 feature 的最新位置
合併前:
main ──●──●
\
feature ●──●──●
合併後(fast-forward):
main ──●──●──●──●──●
↑
直接快進到這裡
---
Three-way 合併(三方合併):
main 也有新的 commit,需要建立一個「合併 commit」
合併前:
main ──●──●──●──●
\
feature ●──●──●
合併後(three-way merge):
main ──●──●──●──●──●──M ← 合併 commit
\ ↗
feature ●──●──●
# 強制建立合併 commit(即使可以 fast-forward)
git merge --no-ff feature/login # 保留分支歷史,建立合併 commit
# 查看合併歷史圖形
git log --oneline --graph --all # 圖形化顯示分支和合併的歷史
Rebase 變基
💡 比喻:搬家重蓋 Rebase 就像把你蓋在舊地基上的房子, 整棟搬到新地基上重蓋。 結果看起來就像是你一開始就蓋在新地基上一樣, 歷史紀錄變得很乾淨、一條直線。
Rebase 前:
main ──●──●──A──B
\
feature ●──●──●
Rebase 後(把 feature 搬到 main 最新的 B 上面):
main ──●──●──A──B
\
feature ●'──●'──●'
重新套用在 B 之後
# 在 feature 分支上執行 rebase
git switch feature/login # 先切換到 feature 分支
git rebase main # 把 feature 的 commit 重新套用在 main 最新的基礎上
# 然後切換回 main 合併(這時會 fast-forward)
git switch main # 切換回 main
git merge feature/login # 因為已經 rebase 過,會是 fast-forward
Merge vs Rebase 比較
┌──────────────┬──────────────────────┬──────────────────────┐
│ │ Merge │ Rebase │
├──────────────┼──────────────────────┼──────────────────────┤
│ 歷史紀錄 │ 保留分支結構 │ 線性歷史(一條線) │
│ 安全性 │ 不會改寫歷史 │ 會改寫 commit hash │
│ 適用場景 │ 公開分支/團隊協作 │ 個人分支/整理歷史 │
│ 比喻 │ 樹枝嫁接 │ 搬家重蓋 │
│ 黃金規則 │ 隨時都可以用 │ 不要對已推送的分支用 │
└──────────────┴──────────────────────┴──────────────────────┘
解決合併衝突
💡 比喻:兩人同時改同一份文件 你和同事同時改了同一行程式碼, Git 無法決定要用誰的版本, 所以它把兩個版本都標出來,讓你手動選擇。
衝突發生時的樣子
# 嘗試合併時出現衝突
git merge feature/login # Git 回報:CONFLICT (content): Merge conflict in Program.cs
// Program.cs 中衝突的樣子:
<<<<<<< HEAD
// 這是 main 分支的版本
var greeting = "你好,歡迎回來!"; // main 的歡迎訊息
=======
// 這是 feature/login 分支的版本
var greeting = "嗨,歡迎登入!"; // feature 的歡迎訊息
>>>>>>> feature/login
解決衝突的步驟
# 步驟 1:查看哪些檔案有衝突
git status # 顯示「both modified」的檔案就是有衝突的
# 步驟 2:打開有衝突的檔案,手動編輯
# 移除 <<<<<<<、=======、>>>>>>> 標記
# 選擇要保留的版本(或兩個都保留/合併)
# 步驟 3:解決後加入暫存區
git add Program.cs # 標記衝突已解決
# 步驟 4:完成合併提交
git commit -m "解決 Program.cs 的合併衝突" # 提交合併結果
# 如果想放棄這次合併
git merge --abort # 取消合併,回到合併前的狀態
VS Code 衝突解決工具
VS Code 會自動偵測衝突並提供按鈕:
┌──────────────────────────────────────────────┐
│ Accept Current Change │ 使用目前分支的版本 │
│ Accept Incoming Change │ 使用合併進來的版本 │
│ Accept Both Changes │ 兩個都保留 │
│ Compare Changes │ 並排比較兩個版本 │
└──────────────────────────────────────────────┘
分支策略
Git Flow
Git Flow 分支策略:
┌────────────────────────────────────────────────────────────┐
│ main ──●────────────────●────────────●── │
│ \ ↗ ↑ │
│ release ●──●──●──●──● release/v2 │
│ ↑ \ │
│ develop ──●──●──●──●──●──●──●──●──●──●── │
│ \ ↗ \ ↗ │
│ feature ●──● ●──● │
│ login signup │
│ │
│ hotfix ────────────────────────●──● │
│ \→ main & develop │
└────────────────────────────────────────────────────────────┘
- main:正式發佈版本
- develop:開發主線
- feature/*:功能分支(從 develop 分出)
- release/*:發佈準備
- hotfix/*:緊急修復(從 main 分出)
GitHub Flow(簡化版)
GitHub Flow 分支策略(適合小團隊和持續部署):
┌────────────────────────────────────────┐
│ main ──●──●──●──M──●──M──●── │
│ \ ↗ \ ↗ │
│ feature-1 ●──● │ │
│ │ │
│ feature-2 ●──● │
│ │
│ 規則: │
│ 1. main 永遠可部署 │
│ 2. 從 main 建立 feature 分支 │
│ 3. 開 Pull Request 請人 review │
│ 4. Review 通過後合併回 main │
│ 5. 合併後立即部署 │
└────────────────────────────────────────┘
Cherry-pick 與 Stash
Cherry-pick(摘櫻桃)
💡 比喻:從其他分支「摘」一個特定的 commit 就像從別人的果園摘一顆特別好的櫻桃到自己的籃子裡, 不需要把整棵樹搬過來。
# 從其他分支挑選特定 commit 套用到目前分支
git cherry-pick abc1234 # 把 commit abc1234 的變更套用到目前分支
# 挑選多個 commit
git cherry-pick abc1234 def5678 # 依序套用兩個 commit
# 只套用變更但不自動提交
git cherry-pick --no-commit abc1234 # 套用變更到工作區,但不自動 commit
# 放棄 cherry-pick
git cherry-pick --abort # 取消目前的 cherry-pick 操作
Stash(暫時擱置)
💡 比喻:把桌上的東西先塞進抽屜 你正在寫功能 A,突然要緊急修 Bug, 但功能 A 還沒寫完不想 commit。 Stash 就是把半成品先「塞進抽屜」, 修完 Bug 後再「從抽屜拿出來」繼續做。
# 暫時擱置目前的變更
git stash # 把所有未 commit 的變更存起來,工作區變乾淨
# 擱置時附上說明
git stash push -m "做到一半的登入功能" # 加上說明方便之後辨識
# 查看所有暫存的東西
git stash list # 列出所有 stash,顯示編號和說明
# 恢復最新的 stash(並從清單移除)
git stash pop # 拿出最新的 stash 並從清單中刪除
# 恢復最新的 stash(但保留在清單中)
git stash apply # 套用最新的 stash,但不從清單刪除
# 恢復特定的 stash
git stash apply stash@{2} # 套用編號 2 的 stash
# 刪除特定的 stash
git stash drop stash@{0} # 刪除編號 0 的 stash
# 清空所有 stash
git stash clear # 刪除所有暫存的變更
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:在 main 分支上直接開發
# 錯誤:直接在 main 上改程式
git switch main # 切換到 main
# ... 開始寫新功能 ...
git commit -m "新增登入功能" # 直接在 main 上提交
# ✅ 正確:先建立分支再開發
git switch main # 先確認在 main 上
git switch -c feature/login # 建立並切換到功能分支
# ... 開始寫新功能 ...
git commit -m "新增登入功能" # 在功能分支上提交
為什麼錯? 直接在 main 上開發,萬一寫到一半要緊急修 Bug,你的半成品程式碼就會混在一起。用分支可以隔離不同的工作。
❌ 錯誤 2:對已推送的公開分支做 rebase
# 錯誤:對已經 push 到遠端的分支做 rebase
git switch main # 切換到 main
git rebase feature/login # 改寫了 main 的歷史!
# ✅ 正確:公開分支用 merge
git switch main # 切換到 main
git merge feature/login # 用 merge 合併,不改寫歷史
為什麼錯? Rebase 會改寫 commit 的 hash,如果其他人已經基於舊的 hash 在工作,他們的歷史就會跟你的對不上,造成混亂。黃金規則:不要 rebase 已經推送到遠端的 commit。
❌ 錯誤 3:衝突解決後忘記 commit
# 錯誤:解決衝突後忘記提交
git merge feature/login # 發生衝突
# ... 手動解決衝突 ...
git add Program.cs # 標記已解決
# 忘記 git commit 了!
# ✅ 正確:解決衝突後要完成提交
git merge feature/login # 發生衝突
# ... 手動解決衝突 ...
git add Program.cs # 標記已解決
git commit -m "解決合併衝突:整合登入功能" # 完成合併提交
為什麼錯? 衝突解決後沒有 commit,Git 仍然處於「合併中」的狀態,後續操作會出問題。
📝 本章重點整理
| 指令 | 用途 | 比喻 |
|---|---|---|
git branch |
管理分支 | 開啟平行宇宙 |
git switch |
切換分支 | 跳到另一個宇宙 |
git merge |
合併分支 | 樹枝嫁接 |
git rebase |
變基 | 搬家重蓋 |
git cherry-pick |
挑選 commit | 摘櫻桃 |
git stash |
暫時擱置 | 塞進抽屜 |