Squash commits: Gitlab & Git

Eric Li
hefemk
Published in
5 min readJul 13, 2024

我時常透過 Gitlab 提供的 Squash commit 功能,在 Merge Request 合併的當下,將工作分支的所有 Commits 壓縮為一,再併入主線當中,讓主線維持簡潔有序,也方便未來 cherry-pick 的執行。

憑藉一時好奇,探討一下如何透過 Git 指令來重現這樣的結果。

Gitlab 的 Squash commit

那麼,所謂 Gitlab 的 Squash commit 是怎樣的體驗?

整個過程大致是這樣:

  1. 當工作分支與目標分支之間沒有衝突,就可以執行合併;若有衝突,則先將目標分支合併回工作分支,例如將 main 先合併回 feature ,並解完衝突。
  2. 勾選 Merge Request 頁面上的 Squash 選項,並可以填入新的 Commit message。
  3. 工作分支上的所有 Commits 會被壓縮為一個 Commit。
    - 作者(Author) 會是發起 Merge Request (MR) 的人 (ref.)。
    - 提交者(Committer) 會是執行壓縮、完成 MR 的人。
  4. 主線 main 將可以觀察到被壓縮的一個 Commit 出現在歷史當中,並且長出小耳朵 (因為有開 --no-ff )。

Git 如何實現 Commits 的壓縮

這個主題已經有許多人談過,搜尋一下不難發現幾種套路。

  1. git rebasegit merge
    - 先透過 rebase 將多個 Commits 整理為一個,再進行合併。
    - squash 僅是 rebase 的部分能力。而 rebase 多半以互動式的方式來執行 (參數 -i ),指令介面相對複雜,即便是透過工具執行也需要有通暢的思緒。
  2. git merge --squash
    - 單純且暴力的用法,一條指令就把工作分支的所有 Commits 壓縮,並完成合併。
    - 但實際上新產生的壓縮 Commit 基底(Base) 與原本的工作分支不同,因此 Git 無法正確判斷原工作分支是否已經完成合併,在刪除工作分支時將有警示。
  3. git reset --soft BASEgit commitgit merge
    - 概念上也是先整理,再合併。
    - 乍看有些詭異的組合技,但理解上反而簡單,也符合 Gitlab Squash commit 的結果。

於是我們就來聊聊這奇妙又單純(?) 的組合技吧!

看看 git reset 的例子

git reset 之流的概念展開來可能略為繁瑣,但並不難理解,操作難度也不高。我們以工作分支 featureA 合併至主線分支 main 為例來說明。

首先切換至工作分支 featureA

git checkout featureA

找出所有 Commits 的起點,可以透過以下指令得到 featureAmain 共同基底的 Commit hash。當然,從各家工具的分支圖也可以人工判讀,不見得要用指令來找。

git merge-base main featureA

有了基底 (起點),就可以利用 git reset --soft 把分支上的所有 Commits 退回工作目錄。

但在這之前,讓我們先思考:壓縮所有的 Commits 是怎麼一回事,會對接下來的原理比較容易理解。

其實真正關鍵的是最後一個 Commit,無論一條分支上隨著時間出現多少增減與調整,最後一個點自然包含所有變更,它是從第一個點到最新的點所造就的最終成果。而 Git 每一個 Commit 記錄了完整的檔案目錄狀態,並不是記錄差異 (當然在儲存方面另有功夫,不是單純 clone 留存,篇幅考量就不展開了),所以這種壓縮/折疊全部 Commits 的操作,不是去計算每次變動加在一起會是什麼,要做的僅是保留最後一個 Commit 的狀態,並且移除其他所有 Commit,這恰巧可以透過 git reset 來實現。

透過 reset 將目前分支的 HEAD 指向基底,基底之後的所有 Commits 就會從歷史移除,而因為使用了 --soft 所以這些被移除的變更會留存在工作目錄當中,讓我們可以再次 Commit。

git reset --soft main

此時會發現如前述所談,分支 featureA 原有的 Commits 除了起點以外都已消失,而那些變更都留存在工作目錄,等待我們重新 Commit。

產生新 Commit 的方式大家都熟悉,唯一多做的是調整 metadata 當中的作者(Author) 欄位,這樣就與 Gitlab 產生出來的結果一致。也讓這個壓縮後的 Commit 資訊更為清晰,記錄誰是作者、誰是提交者(進行壓縮的人)。

git commit --author "Author <author@example.com>" -m "Squashed commit, #123"

至此,一個包含所有變更的新 Commit 誕生!且它仍然存在於分支 featureA 上,因此將它合併回 main 之後,再移除 featureA 也不會有警告,Git 可以明確判斷它有被合併。

git checkout main
git merge featureA --no-ff

延伸探討:Not fully merged 之謎

寫到這邊,篇幅也夠長了,但還有一個令人好奇的題目「Git: Not fully merged 之謎」另開文章與大家分享。

--

--