Git 合併到底使用 Merge 還是 Rebase

git rebase 命令常常因爲江湖上關於它是一種 Git 魔法命令的名聲而導致 Git 新手對它敬而遠之,但是事實上如果一個團隊能夠正確使用的話,它確實可以讓生活變得更簡單。在這篇文章中我們會比較 git rebase 和經常與之相提並論的 git merge 命令,並且在真實典型的 Git 工作流程中識別潛在的可使用 rebase 的場景。

概念概述

首先我們應該明白 git rebase 是用來處理 git merge 命令所處理的同樣的問題。這兩個命令都用於把一個分支的變更整合進另一個分支——只不過他們達成同樣目的的方式不同。

請考慮這個場景,當你開始在一個專有的分支開發新的功能時,另一位團隊成員更新了 main 分支的內容。這將會造成一個分叉的提交歷史,對於任何一個使用 Git 作爲代碼協作工具的人來說都不會陌生。

現在假設 main 分支內新增的內容與你正在開發的新功能有關。爲了把 main 分支裏新增的代碼應用在你的 feature 分支,你有兩種方法:merge 和 rebase。

使用 merge

最簡單的方法就是把 main 分支合併進功能分支:

git checkout feature
git merge main

或者用下面這樣的單行命令:

git merge feature main

這會在 feature 分支中創建一個合併提交,這次提交會連結兩個分支的提交歷史,在分支圖示結構中看起來像下面這樣:

合併操作很友好,因爲它沒有破壞性。現存的分支歷史不會發生什麼改變。這一特性避免了 rebase 操作的所有缺陷(下面會詳細討論)。

但是另一方面來說,這也意味着每當 feature 分支需要應用上游分支的更改時,都會在提交歷史上增加一個無關的提交歷史。如果 main 分支的更新非常活躍,這種操作也會對功能分支的提交歷史產生相當程度的污染。雖然通過複雜的 git log 命令可以減輕這種提交歷史的混亂現狀,但仍然會讓其他開發者對於提交歷史感到費解。

使用 rebase

爲了替代 merge 操作,你也可以把 feature 分支的提交歷史 rebase 到 main 分支的提交歷史頂端:

git checkout feature
git rebase main

這些操作會把 feature 分支的起始歷史放到 main 分支的最後一次提交之上,也達成了使用 main 分支中新代碼的目的。但是,相對於 merge 操作中新建一個合併提交,rebase 操作會通過爲原始分支的每次提交創建全新的提交,從而重寫原始分支的提交歷史。

使用 rebase 操作的最大好處在於你可以讓項目提交歷史變得非常乾淨整潔。首先,它消除了 git merge 操作所需創建的沒有必要的合併提交。其次,正如上圖所示,rebase 會造就一個線性的項目提交歷史——也就是說你可以從 feature 分支的頂部開始向下查找到分支的起始點,而不會碰到任何歷史分叉。這在使用 git log,git bisect 以及 gitk 等命令時更簡單。

不過爲了獲得這種便於理解的提交歷史,卻需要付出兩種代價:安全性和可追溯性。如果不能遵循 rebase 的黃金法則,重寫項目提交歷史會爲協作工作流程帶來潛在的災難性後果。再次,rebase 操作丟失了合併提交能夠提供的上下文信息——所以你就無法知道功能分支是什麼時候應用了上游分支的變更。

可交互式 rebase

可交互式 rebase 讓你在把變更提交給其他分支之前有機會對提交記錄進行修改。這甚至比自動 rebase 操作更強大,畢竟它提供了對於分支提交歷史的完全掌控力。通常來說這一操作的使用場景在於合併功能分支到 main 分支之前,對於功能分支雜亂的提交記錄進行整理。

進行可交互式 rebase 操作,需要向 git rebase 命令傳遞 i 選項參數

git checkout feature
git rebase -i main

執行以上命令會打開一個文本編輯器,其中內容爲分支中需要移動的所有提交列表:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

上面這樣的列表正表示了分支被 rebase 之後其歷史的長相。通過修改 pick 命令或者對提交歷史進行重新排序,你可以讓最終的提交歷史變成任何你希望的樣子。比如說,如果第二次提交修復了第一次提交的什麼 BUG,你可以使用 fixup 命令替代 pick 來把兩次提交壓縮在一起。

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

當你保存並關閉這個文件之後,Git 會根據你的調改結果執行 rebase 操作,根據上面的例子項目歷史會變成下圖這樣:

通過清除那些並不重要的提交歷史可以讓項目整體的歷史更易讀易懂。這一點是 git merge 操作所無法提供的。

rebase 操作黃金法則

一旦你明白了什麼是 rebase,接下來最重要的事情就是要了解什麼情況下不應該使用它。關於 git rebase 的黃金法則就是永遠不要在公共分支上使用它。

舉例來說,想一想如果把 main 分支 rebase 到 feature 分支之上,會發生什麼:

rebase 命令會把 main 分支中的所有提交都放到 feature 分支的提交記錄頂端。問題在於這個改變目前只出現在你的本地倉庫。其他開發者仍然在原來的 main 分支上進行開發。由於 rebase 會產生全新的提交記錄,所以 Git 會認爲現在你本地的 main 分支與所有其他人的產生了分叉。

唯一能夠同步兩個不同的 main 分支的方式就是將其合併起來,這會產生一個冗餘的合併提交,並且這次合併中的大部分提交內容都是相同的(以前的 main 分支和你本地的 main 分支中)。不用說,這下可真讓人疑惑。

所以任何時候要執行 git rebase 命令之前,先確認 “是否有其他人也正在使用此分支?” 如果答案是確定的,那麼你就應該停下來想想有沒有其他非破壞性的操作(比如試試 git revert 命令)。除了這樣的情況之外,重寫提交歷史都是安全的。

Force-Pushing

如果你確實對 main 分支進行了 rebase 操作,然後想把 main 分支推送到遠程倉庫。這時 Git 會因爲本地分支的提交與遠程分支的提交發生了衝突,而阻止你這次的推送。但是,你仍然可以通過使用 --force 選項來強行進行推送,像這樣:

# Be very careful with this command! git push --force

強制推送的結果會讓遠程倉庫的 main 分支使用被你 rebase 過的分支提交歷史,當然這會讓團隊其他成員非常困惑。所以除非你明確知道你在做什麼,否則不要輕易使用強制推送選項。

只有一種情況是屬於 “應當” 使用強制推送命令的,那就是當你向遠程倉庫推送了一個私有分支之後,又做了一些清理工作。此時你大概的想法是:“哦!我發現在還是用現在這個分支的記錄比較合適,不要之前已經推送的那個分支記錄了”。即便如此,確定沒有人與你在這個分支上進行協作仍然是非常重要的一件事情。

工作流實戰

無論團隊規模大小,rebase 操作可以順暢的接入現有團隊的工作流程。在本部分中,我們一起看看在不同的功能開發階段中,rebase 都能提供哪些收益。

在任何一種工作流中,如果我們希望讓 rebase 介入其中,那麼第一步就是爲功能開發創建一個專用分支。這樣可以提供必要的分支結構以便安全地使用 rebase:

本地清理

在現有工作流中包含 rebase 操作的最適合的場景之一是:清理本地正在進行中的開發分支。通過定期使用可交互 rebase 操作,可以清理本分支的提交記錄,讓每一次提交都更加聚焦並有意義。可交互 rebase 操作允許你在寫代碼的時候不用太在意提交歷史,事實上你可以在事後再對提交歷史進行清理。

當使用 git rebase 命令時,有兩種選項可以作爲新的 base:功能分支的父分支(比如 main 分支),或者是本分支內歷史中的某一次提交。第一種情況的示例我們在交互式 rebase 的段落見到過。後一種選項對於修改本分支內的提交歷史則相當有用。比如下面的命令會開啓一次對於最近三次提交歷史的 rebase 操作。

git checkout feature git rebase -i HEAD~3

通過指定 HEAD~3 作爲 rebase 操作的新 base,你並不是在實際移動分支——你只是以交互的方式對 HEAD~3 這次提交之後的三次提交歷史進行重寫。注意這個操作並不會將上游的修改引入 feature 分支:

如果你想對整個 feature 分支歷史進行重寫,那麼應該試試 git merge-base 命令,它會返回給你 feature 分支的原始 base。下面的命令返回原始 base 的 commit ID,獲得之後就可以用於 git rebase 命令的參數:

 git merge-base feature main

像上面這種 rebase 的使用場景非常利於將 git rebase 引入現有的工作流程,畢竟它只會影響本地分支。其他開發者能看到的只是你已經完成之後的作品,那種擁有乾淨提交歷史,易於理解分支內容,便於跟蹤開發過程的優美的分支提交歷史。

不過仍然,只能對私有分支進行此操作。如果你通過同一分支與其他開發者進行協作,那麼這個分支就是公共分支,是不允許重寫提交歷史的。

對於 git merge 操作沒有可替代的方式用來清理本地的提交歷史。

引入上游的修改

在本文最開始的部分,我們討論過如何通過 git merge 或者 git rebase 方式引入上游 main 分支的修改。merge 操作足夠安全,因爲它保留了完整的提交歷史,但是 rebase 操作通過將功能分支的提交歷史移到 main 分支的頂端從而創建了線性的提交歷史。

此種對於 git rebase 操作的使用與清理本地提交歷史類似(也可以同時操作),差別在於在執行過程中會引入上游 main 分支的提交。

請記住 rebase 可以對任何遠端分支進行操作,並不僅限於 main 分支。比如當你需要與其他人協作開發一個功能時,你可以通過 rebase 來引入其他人的開發內容。

比如說,當你和另一個名叫 John 的開發者都對 feature 分支進行了提交動作,在你 fetch 遠程的 feature 分支之後,本地倉庫應該看起來是下圖這樣的:

爲了整合這個分叉,你可以像對待 main 分支一樣:要麼通過 merge 操作將 john/feature 分支合併到本地 feature 分支,或者 rebase 本地 feature 分支到 john/feature 分支的頂端。

請注意這並不與 rebase 的黃金法則發生衝突,因爲只有你本地的 feature 分支的新提交被移動到 john/feature 分支的頂端,新提交之前的所有提交歷史都沒有變化。這就好像說:“把我提交的新內容添加到 John 已經提交的內容之上。” 在大多數情況下,這種操作比使用 merge 操作更符合人類的直覺。

git pull 命令默認行爲是進行一次合併操作,但你可以通過添加 --rebase 選項指定 pull 操作的行爲爲 rebase。

使用 pull request 進行功能審查

如果你使用 pull request 來進行代碼審查工作,那麼在創建了 pull request 之後應該避免使用 git rebase。一旦你創建了 pull request,其他開發者就會來查看你的提交,也就意味着此時的分支算作是一個公共分支了。那麼此時重寫提交歷史,則會讓 Git 和團隊成員無法判斷哪些提交是屬於這個功能的。

引入任何他人的修改時,應該使用 git merge 而不是 git rebase。

因此在提交 pull request 之後進行一次交互式 rebase 來清理提交歷史通常是一個好主意。

整合審查通過的功能

被團隊審查通過的功能代碼,可以先使用 rebase 將新代碼移動到 main 分支的頂端,然後在進行 git merge 合併新功能到 main 分支中。

這個操作跟 rebase 上游分支到本地功能分支類似,只是由於你不能重寫 main 分支的提交歷史,所以你只能在最後通過 git merge 操作來把功能分支的代碼整合進 main 分支。不過在合併之前進行一次 rebase,可以保證這次 merge 操作是可以快速前進的,這樣提交歷史看上去就是完美的線性。這也給你機會可以在真正合並之前進行一次提交歷史的清理。

如果你還不是很適應 git rebase 操作,那麼總是可以利用一個臨時分支來進行 rebase 操作。這樣的話,萬一你不小心搞亂了功能分支的提交歷史,總還有兜底的機會從原始的功能分支再來一遍。就像下面這樣:

git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch

總結

這就是你開始使用 rebase 時所有需要了解的知識了。如果你希望一個乾淨線性的提交歷史,而不是含有衆多合併提交相互交織的提交歷史,那麼應該嘗試在整合分支時使用 git rebase 而不是 git merge。

反過來說,如果你想要保存完整的提交歷史,避免重寫公共提交的歷史,仍然可以堅持使用 git merge。兩者都可以,但至少你現在擁有了另一個選項,可以見機利用 git rebase 的優勢。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3jxG4pdjSiCA1tgo5zau3g