在前一章中,我們學會了如何建立和管理分支。但分支的真正價值在於能夠將不同的開發成果整合在一起。今天我們要學習合併分支以及處理過程中可能出現的衝突。作為一位經驗豐富的工程師,我可以告訴你,掌握衝突解決是從初學者進階到專業開發者的重要里程碑。
什麼是合併?
合併就像是將兩條河流匯聚成一條。在 Git 中,我們將不同分支上的變更整合到一個分支中。想像你和同事分別在寫一本書的不同章節,最終你們需要將所有章節合併成完整的書籍。
Git 提供了兩種主要的整合方式:merge 和 rebase。它們的目標相同,但採用的策略不同,產生的歷史記錄也不一樣。
Fast-forward 合併 vs 三方合併
Fast-forward 合併
這是最簡單的合併情況,發生在目標分支沒有新的提交時:
# 假設的情況
# main: A---B
# feature: B---C---D
git switch main
git merge feature
# 結果:main 直接指向 D
# main: A---B---C---D
實際操作範例:
# 建立並切換到功能分支
git switch -c feature/add-footer
echo "版權所有 2024" > footer.html
git add footer.html
git commit -m "新增網站頁尾"
# 回到 main 分支(沒有新的提交)
git switch main
git merge feature/add-footer
輸出會顯示:
Updating a1b2c3d..e4f5g6h
Fast-forward
footer.html | 1 +
1 file changed, 1 insertion(+)
三方合併
當兩個分支都有新的提交時,Git 會進行三方合併:
# 情況:
# main: A---B---E
# feature: B---C---D
git switch main
git merge feature
# 結果:建立新的合併提交 M
# main: A---B---E---M
# \ /
# feature: C---D
實際操作範例:
# 在 main 分支上進行修改
git switch main
echo "主分支的變更" >> README.md
git add README.md
git commit -m "更新 README"
# 切換到功能分支並修改
git switch feature/user-login
echo "登入功能完成" >> login.js
git add login.js
git commit -m "完成使用者登入功能"
# 回到 main 並合併
git switch main
git merge feature/user-login
系統會提示你輸入合併提交的訊息,預設通常是:
Merge branch 'feature/user-login'
Merge vs Rebase 的差異
Merge:保留歷史脈絡
git switch main
git merge feature/payment
優點:
- 保留完整的歷史記錄
- 可以看出什麼時候進行了合併
- 較安全,不會改變已存在的提交
缺點:
- 歷史記錄可能變得複雜
- 圖形化歷史會有很多分叉
Rebase:線性歷史
git switch feature/payment
git rebase main
git switch main
git merge feature/payment # 這會是 fast-forward
優點:
- 產生線性、乾淨的歷史記錄
- 更容易理解專案的發展過程
- 沒有額外的合併提交
缺點:
- 改寫了提交歷史
- 如果使用不當可能造成問題
- 對已推送的分支使用 rebase 需要特別小心
實際範例比較:
# 使用 merge
git log --oneline --graph
* d4e5f6g (main) Merge branch 'feature/payment'
|\
| * b2c3d4e (feature/payment) 實作支付功能
| * a1b2c3d 新增支付表單
* | c5d6e7f 修復主頁 bug
|/
* 9f8e7d6 初始提交
# 使用 rebase
git log --oneline --graph
* b2c3d4e (main) 實作支付功能
* a1b2c3d 新增支付表單
* c5d6e7f 修復主頁 bug
* 9f8e7d6 初始提交
什麼是衝突?
衝突發生在 Git 無法自動決定如何合併兩個分支的變更時。最常見的情況是兩個分支修改了同一個檔案的同一部分。
想像兩個編輯同時修改一篇文章的同一段落,系統無法決定要採用哪個版本,就需要人工介入決定。
衝突產生的常見情況
- 修改同一行內容
- 一方修改,另一方刪除
- 二進位檔案衝突
- 檔案名稱衝突
衝突解決的完整流程
讓我們模擬一個真實的衝突情況:
步驟 1:建立衝突情況
# 建立測試檔案
git switch main
echo "歡迎來到我們的網站" > welcome.txt
git add welcome.txt
git commit -m "新增歡迎訊息"
# 在 main 分支修改
echo "歡迎來到我們的官方網站" > welcome.txt
git add welcome.txt
git commit -m "更新歡迎訊息為官方網站"
# 建立功能分支並修改同一檔案
git reset --hard HEAD~1 # 回到分歧點
git switch -c feature/welcome-update
echo "歡迎來到我們的專業網站" > welcome.txt
git add welcome.txt
git commit -m "更新歡迎訊息為專業網站"
步驟 2:嘗試合併並遇到衝突
git switch main
git merge feature/welcome-update
你會看到衝突訊息:
Auto-merging welcome.txt
CONFLICT (content): Merge conflict in welcome.txt
Automatic merge failed; fix conflicts and then commit the result.
步驟 3:檢查衝突狀態
git status
輸出會顯示:
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: welcome.txt
no changes added to commit (use "git add" or "git commit -a")
步驟 4:查看衝突內容
cat welcome.txt
你會看到 Git 插入的衝突標記:
<<<<<<< HEAD
歡迎來到我們的官方網站
=======
歡迎來到我們的專業網站
>>>>>>> feature/welcome-update
衝突標記說明:
<<<<<<< HEAD
:目前分支(main)的內容開始=======
:分隔線>>>>>>> feature/welcome-update
:合併分支的內容結束
步驟 5:解決衝突
你有三個選擇:
選擇 1:保留目前分支的版本
echo "歡迎來到我們的官方網站" > welcome.txt
選擇 2:採用合併分支的版本
echo "歡迎來到我們的專業網站" > welcome.txt
選擇 3:手動合併兩個版本
echo "歡迎來到我們的官方專業網站" > welcome.txt
步驟 6:標記衝突已解決
git add welcome.txt
步驟 7:完成合併
git commit
Git 會開啟編輯器讓你輸入合併提交訊息,預設訊息通常已經很合適:
Merge branch 'feature/welcome-update'
# Conflicts:
# welcome.txt
使用合併工具解決衝突
設定合併工具
# 使用 VS Code 作為合併工具
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
# 使用其他常見工具
git config --global merge.tool vimdiff # Vim
git config --global merge.tool meld # Meld(Linux)
git config --global merge.tool opendiff # FileMerge(macOS)
啟動合併工具
git mergetool
這會開啟視覺化的三方比較介面,讓你更容易理解和解決衝突。
複雜衝突的處理策略
多檔案衝突
# 查看所有有衝突的檔案
git diff --name-only --diff-filter=U
# 逐一解決每個檔案
git mergetool file1.js
git mergetool file2.css
git mergetool file3.html
# 或批量處理
git mergetool
二進位檔案衝突
# 對於圖片等二進位檔案,需要手動選擇版本
git checkout --ours image.png # 使用目前分支的版本
git checkout --theirs image.png # 使用合併分支的版本
git add image.png
檔案重新命名衝突
# 當一個分支重新命名檔案,另一個分支修改內容時
git status # 會顯示 "deleted by us" 或 "deleted by them"
# 手動處理
git add renamed-file.js # 加入重新命名的檔案
git rm old-file.js # 移除舊檔案
合併策略選擇
何時使用 Merge
# 適合以下情況:
# 1. 公開的、已推送的分支
# 2. 想保留完整的歷史脈絡
# 3. 團隊偏好看到明確的合併點
git switch main
git merge feature/user-dashboard
何時使用 Rebase
# 適合以下情況:
# 1. 私人的、未推送的分支
# 2. 希望保持線性歷史
# 3. 整理提交歷史
git switch feature/user-profile
git rebase main
git switch main
git merge feature/user-profile # Fast-forward merge
何時使用 Squash and Merge
# 將功能分支的所有提交壓縮成一個
git switch main
git merge --squash feature/small-fix
git commit -m "修復小 bug:登入按鈕樣式問題"
預防衝突的最佳實踐
1. 頻繁同步主分支
# 每天開始工作前
git switch main
git pull origin main
# 切換到功能分支並同步
git switch feature/my-feature
git merge main # 或使用 git rebase main
2. 保持功能分支短命
# 好的做法:小而頻繁的功能分支
git switch -c feature/add-login-button
# 1-2 天完成
git switch -c feature/validate-login-form
# 1-2 天完成
# 避免:長期的大功能分支
git switch -c feature/complete-user-system
# 2-3 週才完成,衝突機率大增
3. 團隊溝通協調
# 當多人修改同一區域時,提前溝通
# 例如:都要修改 header.css 時,先討論分工
進階合併技巧
使用 cherry-pick 選擇性合併
# 只合併特定的提交
git cherry-pick a1b2c3d
# 合併多個提交
git cherry-pick a1b2c3d e4f5g6h
# 合併提交但不自動提交(可以修改)
git cherry-pick --no-commit a1b2c3d
部分檔案合併
# 只合併特定檔案
git checkout feature/payment -- payment.js
git add payment.js
git commit -m "合併支付功能檔案"
互動式 rebase 清理歷史
# 在合併前清理功能分支的提交歷史
git switch feature/user-profile
git rebase -i HEAD~5
# 在編輯器中可以:
# pick -> 保留提交
# squash -> 合併到前一個提交
# reword -> 修改提交訊息
# drop -> 刪除提交
衝突解決工具比較
命令列工具
# 使用 git diff 查看衝突
git diff
# 使用 git show 查看特定提交
git show HEAD
# 使用 git log 查看合併歷史
git log --merge
圖形化工具
# 設定不同的合併工具
git config --global merge.tool kdiff3 # KDiff3
git config --global merge.tool p4merge # Perforce P4Merge
git config --global merge.tool tortoisemerge # TortoiseMerge
git config --global merge.tool beyondcompare # Beyond Compare
推薦工具特色:
- VS Code:免費,整合度高,適合日常使用
- Beyond Compare:功能強大,但需付費
- KDiff3:免費,跨平台,功能完整
- P4Merge:免費,介面直觀
實際專案中的合併工作流程
功能開發完整流程
# 1. 建立功能分支
git switch main
git pull origin main
git switch -c feature/shopping-cart
# 2. 開發過程中定期同步
git fetch origin
git merge origin/main # 或 git rebase origin/main
# 3. 功能完成,準備合併
git switch main
git pull origin main
git merge feature/shopping-cart
# 4. 如果有衝突,解決後繼續
# 如果沒有衝突,直接完成
# 5. 推送並清理
git push origin main
git branch -d feature/shopping-cart
git push origin --delete feature/shopping-cart
緊急修復流程
# 1. 從生產版本建立修復分支
git switch main
git pull origin main
git switch -c hotfix/critical-security-fix
# 2. 快速修復
echo "fixed security issue" > security.patch
git add security.patch
git commit -m "修復關鍵安全漏洞"
# 3. 合併到主分支
git switch main
git merge hotfix/critical-security-fix
# 4. 如果有開發分支,也要合併
git switch develop
git merge hotfix/critical-security-fix
# 5. 立即部署
git push origin main
git push origin develop
常見合併錯誤與解決
錯誤 1:合併錯誤的分支
# 如果合併還沒推送,可以重置
git reset --hard HEAD~1
# 如果已經推送,使用 revert
git revert -m 1 HEAD
錯誤 2:衝突解決不完全
# 檢查是否還有未解決的衝突
git diff --check
# 查看合併狀態
git status
# 如果發現問題,重新開始
git merge --abort
錯誤 3:錯誤的衝突解決
# 如果發現解決錯誤,可以重新解決
git reset --mixed HEAD~1
git merge feature/branch-name
# 重新解決衝突
團隊合併規範建議
Code Review 流程
# 1. 推送功能分支
git push -u origin feature/new-feature
# 2. 建立 Pull Request / Merge Request
# 3. 團隊 Review
# 4. 修改意見後更新
git add .
git commit -m "根據 review 意見修改"
git push
# 5. 核准後合併
合併訊息規範
# 好的合併訊息
git commit -m "Merge branch 'feature/user-authentication'
新增使用者驗證功能,包括:
- 登入/登出功能
- 密碼加密儲存
- 會話管理
- 單元測試覆蓋率 95%
Closes #123"
# 避免無意義的訊息
git commit -m "merge"
git commit -m "fix conflicts"
自動化合併檢查
使用 Git Hooks
# 在 .git/hooks/pre-merge-commit 中加入檢查
#!/bin/sh
# 檢查是否有未解決的衝突標記
if grep -r "<<<<<<< HEAD" .; then
echo "錯誤:發現未解決的衝突標記"
exit 1
fi
# 檢查測試是否通過
npm test
if [ $? -ne 0 ]; then
echo "錯誤:測試未通過"
exit 1
fi
CI/CD 整合
# .github/workflows/merge-check.yml
name: Merge Check
on:
pull_request:
branches: [ main ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: npm test
- name: Check for conflict markers
run: |
if grep -r "<<<<<<< HEAD" .; then
echo "Found unresolved conflict markers"
exit 1
fi
我的合併經驗談
作為一位資深工程師,我想分享一些實戰心得:
1. 衝突不可怕,可怕的是逃避
剛開始遇到衝突時,很多人會感到恐慌。其實衝突是正常的,代表團隊在積極開發。關鍵是要:
- 冷靜分析衝突的原因
- 理解兩個版本的差異
- 做出合理的整合決策
2. 溝通比技術更重要
# 遇到複雜衝突時,不要自己猜測
# 找相關的開發者討論:
git log --oneline file-with-conflict.js # 查看誰修改過
git blame file-with-conflict.js # 查看每行的作者
然後找該作者討論最佳的合併方式。
3. 工具是助手,理解是關鍵
雖然有很多強大的合併工具,但理解衝突的本質更重要:
- 為什麼會產生衝突?
- 兩個版本想要達成什麼目標?
- 如何整合才能保持功能完整?
衝突解決檢查清單
每次解決衝突後,我都會進行以下檢查:
# 1. 確認沒有衝突標記
grep -r "<<<<<<< HEAD" .
grep -r "=======" .
grep -r ">>>>>>> " .
# 2. 執行測試
npm test # 或其他測試指令
# 3. 檢查語法
npm run lint # 或其他程式碼檢查工具
# 4. 本地運行確認功能
npm start # 啟動應用程式測試
# 5. 檢查合併後的程式碼邏輯
git diff HEAD~1 # 查看這次合併的所有變更
小結
合併和衝突解決是 Git 使用中最具挑戰性的部分,但也是最重要的技能之一:
核心概念回顧:
- Fast-forward vs 三方合併:理解不同的合併情況
- Merge vs Rebase:選擇適合的整合策略
- 衝突標記:學會讀懂 Git 的衝突提示
- 解決策略:保留、替換、或整合
重要指令:
git merge branch-name # 合併分支
git rebase branch-name # 變基操作
git mergetool # 啟動合併工具
git merge --abort # 取消合併
git status # 查看合併狀態
最佳實踐:
- 頻繁同步主分支
- 保持功能分支簡短
- 使用合適的合併工具
- 團隊溝通協調
- 建立合併檢查流程
在下一篇文章中,我們將學習遠端倉庫的操作,包括如何與 GitHub、GitLab 等平台協作,以及如何設定和管理多個遠端倉庫。
記住,成為合併衝突解決的專家需要時間和練習。每次遇到衝突都是一次學習機會,不要害怕犯錯,在安全的環境中多加練習,你很快就能掌握這項重要技能。