前言

在軟體開發過程中,版本控制工具是每個開發者都不可或缺的利器,而 Git 以其強大的功能和靈活的操作方式,成為目前最流行的版本控制系統之一。在 Git 中,合併(merge) 是一項極為重要的操作,有時候我們開發完一個新功能(例如一個新功能分支)後,需要將這個新功能合併到主分支像是 master, develop,我們不會直接在主分支上面進行 commit 和 push (往往 repository 的 maintainer 也會禁止這些操作)也因此我們需要發起一個合併請求(Merge Request)來進行合併操作。而這個合併請求有一些不同的策略,理解這些合併策略,不僅能幫助開發者選擇合適的合併方式,還能在團隊協作中保持清晰的版本歷史,避免衝突與混亂。

本篇部落格將深入探討 GitLab 中三種常見的合併方法:Merge Commit、Semi-Linear Merge、Fast Forward Merge。

我會詳細解釋每種方法的操作流程、適用場景及其優缺點,讓你能夠在面對不同的開發需求時選擇最合適的合併策略。希望讀者能夠了解這些常見的合併策略,並能將它們靈活運用於日常開發過程中。

Git Merge

在開始介紹 GitLab 不同的合併策略之前,我們應該要先收集 git merge 的常見用法,如果你使用 VSCode 作為你的 IDE,在使用 merge 的時候可能會跳出以下幾種建議:

  • Merge 基本會看當前的狀況是否有分叉歷史,如果沒有就會自動使用 fast forward merge 進行合併,否則會建立一個 merge commit 進行合併。
  • Fast-forward Merge 如果沒有分叉歷史,才可以使用 fast forward merge 進行合併,否則會拒絕合併。他並不會產生 merge commit。
  • Squash Merge 會將所有的 commit 合併成一個 commit 進行合併,這樣可以保持 commit log 的整潔,但是會失去 commit 的歷史紀錄。
  • No Fast-forward Merge 會強制產生 merge commit,不會使用 fast forward merge 進行合併。

Merge

先來介紹最常見的合併方法:Merge Commit。當我們發起一個合併請求時,GitLab 會將目標分支(通常是 master 或 develop)和源分支(新功能分支)進行合併,並生成一個新的合併提交(Merge Commit)。他是允許目標分支與源分支有不同的歷史

簡單來說使用情境是:

  • 目標分支(如 main)和來源分支(如 feature)有 commit 的分叉歷史
  • 直接 Merge 會產生 Merge Commit,保持完整的開發歷史
  • 想要完整保留開發歷史記錄,但可能產生較雜亂的 commit log

看到這裡,你可能會好奇所謂的分叉歷史是什麼意思?假設 feature 分支從 main 建立後,兩邊各自有不同的 commit,feature 在開發的過程中 main 有了一個 commit 是 不存在於 feature 分支的內容,這時候 feature 和 main 就會有分叉的歷史,這時候就會產生 Merge Commit。

1
2
3
4
5
6
*  c5 (main)  <-- main 上有新的 commit
| * f3 (feature)
| * f2 (feature)
| * f1 (feature)
|/
* c1 (main)

執行:

1
2
git checkout main
git merge feature

結果:

  • m6 是 Merge Commit,它將 main 和 feature 分叉的歷史合併在一起。
  • main 分支 沒有改變 feature 的 commit 順序,保留了它的獨立歷史。
1
2
3
4
5
6
7
8
*   m6 (main)  <-- Merge Commit
|\
| * f3 (feature)
| * f2 (feature)
| * f1 (feature)
|/
* c5 (main)
* c1 (main)

Fast Forward Merge

Fast-Forward 白話的意思就是快速向前,當我們進行合併時,如果目標分支(如 main)和來源分支(如 feature)的 commit 歷史是線性的,也就是沒有發生像上面例子 feature 在開發的過程中, master 有了一個 m5 的 commit 是 不存在於 feature 分支的內容,那麼 Git 就可以直接將目標分支指向來源分支的最新 commit,而不需要產生新的 Merge Commit。這樣的合併方式稱為 Fast Forward Merge。

簡單來說:

  • 目標分支和來源分支的 commit 歷史是線性時,可以使用 Fast Forward Merge
  • 他會讓目標分支直接指向來源分支的最新 commit,不會產生 Merge Commit
  • 目標分支和來源分支的 commit 歷史是線性的.想要保持整潔的 commit log

舉例來說

1
2
3
4
5
*  f3 (feature) <-- feature 沒有遺漏 main 的 commit
* f2 (feature)
* f1 (feature)
| * c5 (main) <-- main 的 HEAD 指向 c5
| * c1 (main)

在這個案例執行 git merge feature --ff-only 或是 git merge feature 會產生相同的結果,main 分支會直接指向 feature 的最新 commit,形成 fast forward merge,不會有任何枝節點產生,會是完整的一條線,因為這兩個分支是沒有分叉的歷史。

1
2
3
4
5
*  f3 (main, feature)  <-- main 直接移動到 feature 的 HEAD
* f2
* f1
* c5
* c1

No Fast Forward Merge

在上面的例子中,如果你希望強制產生 Merge Commit,讓人知道在這個時間點有一個人 merge 了一個分支到 main,可以使用 --no-ff 選項,這樣就會強制產生 Merge Commit,不會使用 Fast Forward Merge 進行合併。

簡單來說:

  • 強制產生 Merge Commit
  • 保持完整的開發歷史記錄,使用 git log 可以看到分支的分叉歷史
  • 可能產生較雜亂的 commit log

舉例來說

1
2
3
4
5
*  f3 (feature) <-- feature 沒有遺漏 main 的 commit
* f2 (feature)
* f1 (feature)
| * c5 (main) <-- main 的 HEAD 指向 c5
| * c1 (main)

在這個案例執行 git merge feature --no-ff 會產生 Merge Commit,如下所示:

  • main 沒有直接 “快進” 到 feature 的最新 commit
  • 因為我們沒有使用 squash 所以他可以保留 feature 分支的 commit 歷史,但是會產生一個 Merge Commit。
1
2
3
4
5
6
7
8
*   M  (main)  <-- Merge Commit,main HEAD 指向這裡
|\
| * f3 (feature)
| * f2
| * f1
|/
* c5
* c1

Squash Merge

如果今天你希望將所有的 commit 合併成一個 commit 進行合併,這樣可以保持 commit log 的整潔,像是有多位 developer 開發多個功能,例如當前有三個功能正在開發,分別是 feature1, feature2, feature3 分支,每個分支裡面都很雜,包含很多細節的 commit,甚至裡面有一些測試的 commit 或是錯誤的內容,如果你是這個 feature 的 owner 沒有其他開發者跟你共同開發,這個 feature 的 commit 可能會很雜亂,如果把這些 feature 合併進去 master 會讓整個 commit log 變得很不堪。

如果我們不希望這種事發生,希望每次 feature merge 進去 main 只會有一個 commit 就是把整個最終成品都寫在一個 commit 裡面,這時候就可以使用 squash merge 進行合併。

簡單來說:

  • 將所有的 commit 合併成一個 commit 進行合併
  • 保持 commit log 的整潔
  • 但會失去 feature 分支開發過程中 commit 的歷史紀錄

舉例來說,如果我們目前有三個 feature 正在開發:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main: 
* c1 (main)

feature1:
* f1-3 (修復 bug)
* f1-2 (調整 API)
* f1-1 (新增功能)

feature2:
* f2-2 (優化效能)
* f2-1 (新增 UI)

feature3:
* f3-3 (修正錯字)
* f3-2 (重構代碼)
* f3-1 (新增功能)

當合併 feature1、feature2、feature3 後,main 會變得很亂,這樣會讓 git log 變得混亂,難以追蹤主要的 feature 更新。

1
2
3
4
5
6
7
8
9
*  f3-3 (修正錯字)
* f3-2 (重構代碼)
* f3-1 (新增功能)
* f2-2 (優化效能)
* f2-1 (新增 UI)
* f1-3 (修復 bug)
* f1-2 (調整 API)
* f1-1 (新增功能)
* c1 (main)

但是我們如果使用 squash:

1
2
3
4
5
6
git merge --squash feature1
git commit -m "Add feature1"
git merge --squash feature2
git commit -m "Add feature2"
git merge --squash feature3
git commit -m "Add feature3"

結果:這樣我們的 commit log 變得非常乾淨,每個 feature 只佔一個 commit,避免了混亂的 commit 記錄。

1
2
3
4
*  Add feature3
* Add feature2
* Add feature1
* c1 (main)

GitLab Merge Request

現在我們知道了 Git 中常見的合併方法,接下來我們來看看 GitLab 中的三種合併策略:Merge Commit、Semi-Linear Merge、Fast Forward Merge。可以看到下圖的內容,他其實有簡單地做一些介紹

  1. Merge Commit(–no-ff)

    • 產生一個 Merge Commit,保留完整的開發歷史(即使 feature 分支可以 fast-forward 也不會省略 merge commit)。
    • 不強制線性歷史,允許分叉的 commit 存在(可能會有多條 commit 歷史並存)。
    • 適合 保留開發歷史、清楚標示 feature 分支的合併點
  2. Semi-Linear Merge(Rebase + --no-ff)

    • 要求 feature 分支 rebase 到最新的 main 分支,確保 feature 分支是線性的。
    • 然後 使用 --no-ff 進行合併,產生一個 Merge Commit。
    • 這樣做的好處是,歷史紀錄保持線性,但仍然有 Merge Commit 作為開發階段的標記
    • 適合 希望有線性歷史,但又想保留 Merge Commit 的專案。
  3. Fast Forward Merge(–ff-only)

    • 如果 feature 分支可以 fast-forward,則直接移動 main 分支的 HEAD 到最新的 feature commit,然後產生 Merge Commit
    • 如果是使用 squash 則是將所有的 commit 合併成一個 commit 進行合併後,再產生 Merge Commit。
    • 需要 確保 feature 分支與 main 沒有分叉歷史,否則需要開發者手動 rebase 來確保可以 fast-forward。
    • 適合 追求最簡潔的 commit 歷史,不希望有額外的 Merge Commit 的情境,例如小型專案或 頻繁 CI/CD 的環境

GitLab 三種 Merge 策略比較

Merge Commit Semi-Linear Merge Fast Forward Merge
Merge 指令 git merge --no-ff git rebase main + git merge --no-ff git merge --ff-only
是否產生 Merge Commit ✅ 會產生 ✅ 會產生 ❌ 不會產生
是否要求 rebase ❌ 不要求 ✅ 需要先 rebase ✅ 需要先 rebase
歷史是否線性 ❌ 允許分叉 ✅ 強制線性 ✅ 強制線性
適用情境 保留完整開發歷史 兼顧線性歷史與 Merge Commit 讓 commit 歷史最乾淨
優點 清楚標示合併點,保留詳細的開發歷史 歷史清晰,可保留 Merge Commit 來標示合併點 Commit 歷史最乾淨,無額外 Merge Commit
缺點 Merge Commit 可能過多,歷史較雜亂 需要開發者主動 rebase 可能導致歷史紀錄與原來 feature 分支不同,影響開發者追蹤歷史

補充:Merge Train

看到這裡你應該基本上就搞懂了 GitLab Merge 的三種方法,但是你在 Fast Forward Merge 還有一段訊息:

If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts. What are merge trains?

所謂的 merge trains 就是在應對一種狀況是,當有多個開發者在不同的 feature 分支上進行開發,這時候可能會發生多個 Merge Request 同時進行合併,這時候就會有一個 merge trains 把每個 Merge Request 放入一個 queue 中,依序進行合併,這樣可以避免多個 Merge Request 同時進行合併,造成衝突。

🚆 Merge Train 的運作方式

  1. 第一個 MR 進入 Merge Train

    • GitLab 會執行一個測試 Pipeline,來確認這個 MR 和目標分支合併後是否能順利運行。
  2. 加入更多 MR 到 Merge Train

    • 如果第二個 MR 被加入 Merge Train,GitLab 會啟動新的 Pipeline,這個 Pipeline 會測試第一個 MR + 第二個 MR + 目標分支是否能一起運作。
    • 如果加入第三個 MR,則 Pipeline 會測試 第一個 + 第二個 + 第三個 + 目標分支,以此類推。
    • 這些 Pipeline 會同時進行(並行運行),但只有當前面的 MR 合併成功後,後面的 MR 才能繼續合併。

🚨 如果某個 MR 的 Pipeline 失敗怎麼辦?
假設有三個 MR(A、B、C) 加入 Merge Train,分別啟動這三個測試 Pipeline:

  1. A + 目標分支
  2. A + B + 目標分支
  3. A + B + C + 目標分支

如果 B 的 Pipeline 失敗了:

  • A 繼續測試,不受影響。
  • B 被移除,不會合併。
  • C 的 Pipeline 取消,然後重新啟動新的 Pipeline,這次測試的是 A + C + 目標分支(不包含 B)。

當 A 測試成功並合併後:

  • C 會繼續測試,但這次它的測試基礎會是最新的主分支(已經包含 A 的變更)。
  • 之後新加入的 MR 也會基於這個最新的主分支來測試。

✅ Merge Train 的好處

  • 確保每個 MR 在合併前都經過測試,減少合併後的問題。
  • 自動排序 Merge 順序,避免手動處理合併衝突。
  • 所有 MR 都是基於最新的主分支進行測試,減少出錯機率。

如何啟用 Merge Train?

  • 必須擁有 Maintainer 權限
  • 不能是 External REpository
  • 必須針對 merge request 設定 pipeline,.gitlab-ci.yml 會有一條 stage 的 rules 設定 if: $CI_PIPELINE_SOURCE == 'merge_request_event' 來針對 merge request 產生一個 pipeline 來驗證是否可以接受 Merge Request
  • 必須啟用 Enable merged results pipelines 也就是只有 pipeline 通過才能進行合併

Reference