Git 合並提交:附範例的指南

「早提交,常提交」是在使用 Git 的軟件開發中流行的一句口號。這樣做可以確保每個變更都記錄得非常清楚,增強合作,並使追踪項目進展變得更加容易。然而,這也可能導致提交過多。

這就是合并提交的重要性所在。合并提交是将多个提交記錄合並為一個單一的、凝聚的提交。

例如,假設我們在一個實現登錄表單的功能上工作,並創建了以下四個提交:

一旦功能完成,對於整個項目來說,這些提交過於詳細。我們未來不需要知道在開發過程中遇到了一個已經修復的bug。為了確保主分支的歷史記錄乾淨,我們將這些提交壓縮為單個提交:

如何在Git中壓縮提交:交互式重置

壓縮提交最常見的方法是使用交互式重置。我們通過以下命令啟動它:

git rebase -i HEAD~<number_of_commits>

<number_of_commits>替換為我們想要壓縮的提交數量。

在我們的情況下,我們有四個提交,所以命令是:

git rebase -i HEAD~4

執行此命令將打開一個交互式命令行編輯器:

上面部分顯示提交記錄,下面則包含如何合並提交的評論。

我們看到四個提交。對於每一個,我們必須決定執行哪個命令。我們關心的是pickp)和squashs)命令。將這四個提交合並為一個提交,我們可以選擇第一個,然後合並剩下的三個。

我們通過修改每個提交之前的文字來應用命令,具體來說,將第二、第三和第四個提交的pick改為ssquash。為了進行這些編輯,我們需要在命令行文本編輯器中進入“插入”模式,按鍵盤上的i鍵:

按下i後,底部將出現文字-- INSERT --,表示我們已經進入插入模式。現在,我們可以使用箭頭鍵移動游標、刪除字符,並像在標準文本編輯器中一樣打字:

一旦我們對變更感到滿意,我們需要按鍵盤上的Esc鍵來退出插入模式。下一步是保存我們的變更並退出編輯器。為此,我們首先按:鍵來告知編輯器我們打算執行一個命令:

現在編輯器底部會出現一個分號:,提示我們輸入命令。為了保存變更,我們使用命令w,這代表”寫入”。為了關閉編輯器,我們使用q,這代表”退出”。這些命令可以結合起來一起輸入wq

為了執行命令,我們按Enter鍵。這個動作會關閉當前編輯器並打開一個新的編輯器,讓我們為新壓縮的提交輸入提交信息。編輯器會顯示一個默認信息,包括我們正在壓縮的四個提交的信息:

我建議修改訊息以準確反映這些合並提交所實施的變更——毕竟,合并的目的就是要維護一個乾淨且容易閱讀的歷史。

要與編輯器互動並編輯訊息,我們再次按i鍵進入編輯模式,並按自己的喜好編輯訊息。

在這種情況下,我們將提交訊息替換為”實現登錄表單”。要退出編輯模式,我們按Esc鍵。然後按:鍵保存變更,輸入wq命令,並按Enter鍵。

如何查看提交歷史

通常,回顧整個提交歷史可能會很困難。要查看提交歷史,我們可以使用git log命令。在提到的例子中,在進行合並之前,執行git log命令會顯示:

要導航提交記錄列表,請使用上下箭頭鍵。要退出,請按q

我們可以使用git log來確認 squash 的成功。在 squash 後執行它將顯示一個帶有新信息的單一提交:

推送已壓縮的提交

上面的命令將對本地倉庫進行操作。要更新遠程倉庫,我們需要推送我們的變更。然而,因為我們改變了提交歷史,所以需要使用--force選項進行強制推送:

git push --force origin feature/login-form

強制推送將覆蓋遠程分支上的提交歷史,並可能會干擾正在該分支上工作的其他人。在這之前與團隊溝通是一個好的實踐。

一種更安全的強制推送方式,可以減少干擾合作者的風險,是使用--force-with-lease選項:

git push --force-with-lease origin feature/login-form

這個選項確保我們只有在遠端分支從我們上次抓取或拉取以來沒有更新時,才會進行強制推送。

壓縮特定提交

想像我們有五個提交:

假設我們想保留第1、2和5個提交,並將第3和4個提交壓縮。

在使用交互式重置基時,標記為壓縮的提交將與其直接前一个提交合併。在這種情況下,意味著我們想將Commit4壓縮,使其合併到Commit3

為了做到這點,我們必須啟動一個交互式的 rebase,包括這兩個 commit。在這種情況下,三個 commit 就足夠了,所以我們使用以下命令:

git rebase -i HEAD~3

然後,我們將 Commit4 設為 s,這樣它就會與 Commit3 合並:

執行這個命令並列出 commits 後,我們觀察到 commit 3 和 4 已經被合並在一起,而其餘的則保持不變。

從特定的 commit 開始合並

在命令 git rebase -i HEAD~3 中,HEAD 部分是最新 commit 的簡寫。語法 ~3 用於指定某個 commit 的祖先。例如,HEAD~1 指的是 HEAD commit 的父 commit。

在交互式重置基操作中,考虑的提交包括指定命令中提交的所有祖先提交。注意,指定的提交不包括在内:

我们可以直接指定一个提交哈希,而不是使用HEAD。例如,Commit2的哈希为dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf,因此命令:

git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf

将会开始一个重置基操作,考虑所有在Commit2之后的提交。因此,如果我们想从特定的提交开始重置基并包括该提交,我们可以使用以下命令:

git rebase -i <commit-hash>~1

解决合并提交时的冲突

当我们合并提交时,将多个提交的更改合并为一个提交,如果更改相互重叠或显著分歧,可能会导致冲突。以下是一些可能产生冲突的常见情况:

  1. 重疊變更:如果正在被合併的兩個或更多個提交修改了同一個文件或相關行數的相同行,Git 可能無法自動調和解這些變更。
  2. 變更狀態不同:如果一個提交添加了某段代碼,而另一個提交修改或刪除了這段相同的代碼,合併這些提交可能會導致需要解決的衝突。
  3. 重命名與修改:如果一個提交重命名了一個文件,而後續的提交對舊名稱進行了更改,合併這些提交可能會使 Git 混亂,導致發生衝突。
  4. 二進制檔案變更:二進制檔案使用基於文字的diff工具合並效果不佳。如果多個提交更改了同一個二進制檔案,我們嘗試將它們壓縮時,可能會發生衝突,因為Git無法自動調和這些更改。
  5. 複雜的歷史:如果提交具有多個合並、分支或重置基點的複雜歷史,壓縮它們可能會因為更改的非線性特性而導致衝突。

在壓縮過程中,Git將試圖一個接一個地應用每個更改。如果在過程中遇到衝突,Git將暫停並讓我們解決它們。

衝突將會被標記為衝突標記 <<<<<<>>>>>>。為了解決這些衝突,我們需要打開文件並手動解決每一個衝突,選擇我們想要保留的代碼部分。

解決衝突後,我們需要使用 git add 命令將解決後的文件進行暫存。然後我們可以使用以下命令繼續進行變基:

git rebase --continue

有關 Git 衝突的更多資訊,請參閱這篇教程:如何在 Git 中解決合併衝突

變基 squash 的替代方案

指令 git merge --squash 是一種將多個提交合並為單一提交的替代方法,與 git rebase -i 相似。當我們想要將某個分支的變更合並到主分支,並將所有個別提交壓縮為一次提交時,此指令尤為有用。以下是如何使用 git merge 進行合並壓縮的概述:

  1. 我們切換到目標分支,即我們要將變更整合進去的分支。
  2. 我們執行指令 git merge --squash <分支名稱>,將 <分支名稱> 替換為分支的名稱。
  3. 然後使用 git commit 提交變更,以創建一個單一提交,代表特性分支中的所有變更。

例如,我們想要將分支 feature/login-form 的更改一次性作為一個提交合並到 main

git checkout main git merge --squash feature-branch git commit -m "Implement login form"

與使用 git rebase -i 相比,這種方法的局限性在於:

  • 控制的粒度: 對個別提交的控制較少。使用 rebase 時,我們可以選擇要合並哪些提交,而合並則強制將所有更改合併為一個提交。
  • 中間歷史:當使用合併時,特性和分支的個別提交歷史會在主分支中丟失。這可能會使得追蹤特性開發期間所做的增量更改變得更加困難。
  • 預提交審查:由於它將所有更改作為一個變更集進行暫存,我們無法在壓縮之前單獨審查或測試每個提交,與交互式重置基線不同,在交互式重置基線過程中,可以按順序審查和測試每個提交。

結論

將頻繁且小的提交納入開發工作流程中能促進合作並提供清晰的文檔記錄,但這也可能會使得專案歷史變得雜亂。將提交壓縮能夠取得平衡,保存重要的里程碑,同時消除小範圍迭代變更的噪音。

要了解更多關於Git的知識,我推薦以下資源:

Source:
https://www.datacamp.com/tutorial/git-squash-commits