1. 程式人生 > >Merging 和 Rebasing 的大比拼

Merging 和 Rebasing 的大比拼

**雖然 merging 和 rebasing 在 git 中相似時,但他們提供不同的功能。為了讓你的歷史儘可能的乾淨和完整,你應該知道以下幾點。** `git rebase` 命令已 神奇的 Git voodoo 而聞名,初學者應該遠離它,但它實際上可以讓開發團隊在使用時更加輕鬆。在本章中,我們將 把 `git rebase` 和與之有關聯的 `git merge` 命令相比較 ,並在典型的 [Git 工作流](https://bitbucket.org/product?utm_source=dzone&utm_medium=paid-content&utm_content=merging-vs-rebasing&utm_campaign=bitbucket_adexp-bbtofu_dzone-syn-content) 中重新定位,識別其所有潛在的機會。 ## 概述 首先要明白關於 `git rebase` 的事情是它像 `git merge` 一樣解決相同的問題。git rebase 和 git merge 一樣都是被設計用於從一個分支獲取併合併到當前分支,但是他們採取不同的工作方式。 考慮一下,當你開始在 一個專用的分支上開發新特性,與此同時另一個團隊成員用新的提交來更新了 `master` 分支時,會發生什麼呢?這會導致分叉的歷史記錄,對於這個問題,使用[ Git 作為協同工具](https://bitbucket.org/product?utm_source=dzone&utm_medium=paid-content&utm_content=merging-vs-rebasing&utm_campaign=bitbucket_adexp-bbtofu_dzone-syn-content)的任何人來說都應該很熟悉。 ![](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103204451-460442269.svg+xml) 現在,假設你在工作時在 `master` 上的新提交與新特性相關。為了將新提交合併到你的 `feature` 分支上,你有兩種選擇:merging 或者 rebasing。 ### Merge 選項 最簡單的選項是使用以下命令將 `master` 分支合併到 `feature` 分支: ``` git checkout feature git merge master ``` 或者,你可以簡化成一句: ``` git merge master feature ``` 這將在 `feature` 分支上建立一個新 “ 合併提交 ” ,並把兩個分支的歷史聯絡在一起。分支結構顯示如下: ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103204630-1533051764.svg+xml) Merging 之所以好是因為它是一個不可逆的操作。在任何情況下,現有分支不能被更改。這避免了所有 rebasing 的潛在陷阱(詳見下文)。 另一方面,這也意味著每次需要合併上游更改時, `feature` 分支都將有一個額外的 merge 提交產生。如果 `master` 非常活躍,這可能破壞你全部的 feature 分支的歷史。使用高階的 `git log` 選項來減緩這個問題是有可能的,也讓其他開發人員很難理解這個專案的歷史記錄。 ### Rebase 選項 作為 merging 的一個替代品,你可以使用以下命令將 `feature` 分支合併到 `master` 分支: ``` git checkout feature git rebase master ``` 這將整個 `feature` 分支從 `master` 分支的頂端開始,有效地將所有新的提交合併到主分支中。但是,並不是使用合併提交,而是通過為每個在原始分支上的提交建立全新的提交來重寫專案歷史。 ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103204774-811275905.svg+xml) rebasing 最主要的益處是你將獲得一個十分乾淨整潔的專案歷史。首先,它通過 `git merge` 排除多餘的 merge 提交需求;其次,正如你在上圖所看到的那樣,rebasing 也會產生完美線性的專案歷史記錄—你可以順著 `feature` 一直到專案的起始位置而沒有任何分支。可以方便的使用 `git log` ,`git bisect` 和 `gitk` 追蹤提交記錄。 但是,對於新的提交歷史有兩點需要權衡:安全性和可追溯性。如果你不遵循 Rebasing 的黃金法則,為你的協作工作流重寫專案歷史可能會成為潛在的災難。另外,不重要的是,rebasing 會丟失合併提交所提供的上下文—你不能看到何時合併到 feature 分支中的上游變化。 ### 互動式的 Rebasing 當他們移動到新的分支上,互動式合併給你機會來修改提交。自從它提供完全控制整個分支的提交歷史之後,它比自動合併更強大。具有代表性的,在合併一個 feature 分支到 `master` 時,它是被用來清除錯誤的歷史。 要開始一個互動式的重基會話,請將 `i` 選項傳遞給 `git rebase` 命令: ``` git checkout feature git rebase -i master ``` 這將開啟一個文字編輯器列出所有要被移動的提交: ``` pick 33d5b7a Message for commit #1 pick 9480b3d Message for commit #2 pick 5c67e61 Message for commit #3 ``` 此列表準確定義了執行 rebase 後分支的外觀。通過改變 `pick` 命令或調整條目順序來改變分支的提交歷史,你可以讓分支看起來像任何你想要的樣子。舉例說,如果第二次提交是為了修復第一次提交中的一個小問題,你可以使用 `fixup` 命令把他們簡化成一個簡單的命令: ``` pick 33d5b7a Message for commit #1 fixup 9480b3d Message for commit #2 pick 5c67e61 Message for commit #3 ``` 當你儲存並關閉檔案時,Git 將根據你的指令來執行 rebase ,從而產生如下所示的專案歷史記錄: ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103205006-529254533.svg+xml) 像這樣排除不重要的提交使你的特性歷史相當易懂。這一點是 `git merge` 無法比擬的。 ## Rebasing 的黃金規則 一旦你明白什麼是 rebasing ,最重要的事情是學習什麼時候不用它。 `git rebase` 的黃金法則是永遠不要在公有分支上使用它。 舉例說,想象一下如果你將 `master` 分支合併到 `feature` 分支上會發生什麼: ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103205278-595955114.svg+xml) rebase 操作將 `master` 中所有提交移動到 `feature` 的頭部,但問題是這一切都發生在你的倉庫中。其他開發者依然在原來的 `master` 分支上繼續工作。自從 rebasing 產生了全新的提交,Git 將會認為你的 `master` 分支的歷史記錄與其他人的歷史記錄不同。 使兩個 `master` 分支 同步的唯一方法是將他們合併到一起,導致出現一個額外的合併操作和兩組都包含相同改變(最原始的那個,和那些來自你重新建立的分支)的提交。不用說,這是一個非常混亂的場景。 因此,在你執行 `git rebase` 之前,一定要問自己,“還有其他人在看這個分支嗎?”,如果回答是肯定的,那麼把你的手從鍵盤上拿開並開始考慮讓你的改變沒有破壞性(例如, `git revert` 命令)。否則,你可以隨心所欲地重寫歷史。 ### Force-Pushing 如果你嘗試將合併的 `master` 分支推送到遠端庫中,Git 將防止你這樣做,因為它與遠端 `master` 分支有衝突。但是,你可以通過傳遞 `--force` 標誌來強制推送,就像這樣: ``` # Be very careful with this command! git push --force ``` 該操作會將遠端倉庫的 `master` 分支替換為 rebase 過的 `master` 分支,這會給團隊的其他成員帶來困擾。因此,當你確切的知道你要做什麼的時候,才要非常小心的使用這些命令。 推送一個私有新特性分支到遠端倉庫(例如,用於備份)。這就好像是說,“哎呦,我不想推送 feature 分支的原始版本,拿當前的版本替換吧。”再強調一次,沒有人在 feature 分支的原始版本中工作是很重要的。 ## 工作流演練 Rebasing 能夠根據團隊的需要或多或少的被合併到你現存的 [Git 工作流](https://bitbucket.org/product?utm_source=dzone&utm_medium=paid-content&utm_content=merging-vs-rebasing&utm_campaign=bitbucket_adexp-bbtofu_dzone-syn-content) 中。在這個選項中,我們將檢查 rebasing 提供在不同階段的 feature 分支開發的好處。 在任何工作流中,首先第一步是利用 `git rebase` 為每一個 feature 建立一個專用的分支。這給你必要分支結構來安全使用 rebasing : ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103205773-1641184355.svg+xml) ### 本地清除 最好的方法之一是合併 rebasing 到你的 [工作流](https://bitbucket.org/product?utm_source=dzone&utm_medium=paid-content&utm_content=merging-vs-rebasing&utm_campaign=bitbucket_adexp-bbtofu_dzone-syn-content) 以此來清理本地正在進行的 feature 分支。通過定期的執行一個互動式的 rebase ,你可以確保每一個在你的 feature 分支中的提交是集中且有意義的。這將讓你編寫你自己的程式碼而不需要在獨立提交中擔心破壞它—你可以在事後修復它。 當呼叫 `git rebase` ,對於新的分支你有兩個選項:feature 父類分支(舉例說,`master` 分支),或者在你的 feature 分支中較早的提交。我們查看了在 *互動式的 Rebasing* 章節中首個選項的示例 。當你僅僅需要修復最新提交時,後者的選擇最好。舉例說,互動式 rebase 的最後3次提交顯示如下: ``` git checkout feature git rebase -i HEAD~3 ``` 通過指定 `HEAD~3` 作為新的基礎,事實上你並沒有移動分支—你只是互動式的重寫了接下來的3次提交。請注意,這不會將上游更改合併到 `feature` 分支。 ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103205890-1993534476.svg+xml) 如果你想使用這個方法重寫整個 feature, `git merge-base` 命令對於找到 `feature` 分支的原始起始點非常有用。以下返回原始起始點的提交 ID ,然後傳遞給 `git rebase` : ``` git merge-base feature master ``` 互動式 rebasing 的作用在於當他僅僅影響本地分支時,它是一個 引進 `git rebase` 到工作流中的好方式。其他開發人員唯一能看到的是你最後提交的成果,這應該是一個簡單且易於理解的 feature 分支歷史記錄。 但是在剛開始,這僅僅只為私有 feature 分支工作。如果你藉助相同 feature 分支與其他開發者協作,分支是共有的,你也不被允許重寫它的歷史記錄。 沒有 `git merge` 之外的其他選擇時可以使用互動式 rebase 來清除本地提交。 ### 合併上游更改到 Feature 中 在開篇章節中,我們知道了 feature 分支如何使用 `git merge` 或 `git rebase` 合併 `master` 分支的上游提交。當 rebasing 通過移動你的 feature 分支到 `master` 分支的頭部來建立一個線性歷史時,Merging 是一個用於保護你倉庫的整個歷史記錄的安全選項。 `git rebase` 的作用與本地清除相似(能夠同時被執行),但是在此過程中,它合併了 `master` 的上游提交。 牢記,遠端分支取代 `master` 分支是完全合法的。這發生在其他開發者在同一個 feature 分支上協作時和你需要合併他們的更改到你的倉庫中時。 舉例說明,如果你和一個名為 John 的開發人員添加了對 `feature` 分支的提交,從 John 的倉庫中獲取遠端 `feature` 分支後,你的倉庫看起來像如下所示: ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103206068-83717299.svg+xml) 你可以用與 `master` 分支整合上游更改相同的方法來解決這個分叉:或者你本地的 `feature` 分支與 `john/feature` 分支合併,或者 rebase 你本地 `feature` 分支到 `john/feature` 分支的頭部。 ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103206366-1114698455.svg+xml) 請注意,任何事情在未更改之前,rebase 不能違反 *Rebasing 的黃金法則* ,因為 `feature` 僅僅移動了本地提交。這就好像是在說,“將我的更改新增到 John 已經完成了的操作中。” 在大多數情況下,這比通過合併提交與遠端分支同步更為直觀。 預設情況下, `git pull` 命令執行合併,但是你可以強制通過使用 rebase 的 `--rebase` 選項整合遠端分支。 ### 使用 Pull 請求檢驗 feature 分支 如果你使用 Pull 請求作為程式碼的審計過程,建立的 pull 請求之後,你需要避免使用 `git rebase` 。一旦你發出 pull 請求,其他開發人員就能看到你的提交,這就意味著它是一個公有分支。重寫它的歷史記錄將使 Git 和你的隊友無法追蹤到任何新增到 feature 分支上的後續提交。 任何來自其他開發者的更改需要使用 `git merge` 取代 `git rebase` 來被合併。 為此,在提交你的 pull 請求之前,使用互動式 rebase 清理你的程式碼,通常是一個好主意。 ### 整合認可的 feature 在 feature 分支被你的團隊認可之後,在使用 `git merge` 整合 feature 分支到主程式碼庫之前,你有一個 rebasing feature 分支到 `master` 分支的選項。 合併上游更改到 feature 分支是一個類似的情況,但是,自從你不被允許在 `master` 中重寫提交,你最後不得不使用 `git merge` 來整合 feature 分支。然而,通過在合併之前執行 rebase 確保 merge 將快速進行,形成完美的線性歷史。這也給了你在 pull 請求期間將任何後續提交塞入到 feature 分支中的機會。 ![img](https://img2020.cnblogs.com/other/626506/202007/626506-20200728103206551-1826061464.svg+xml) 如果你對 `git rebase` 感到不太舒服,你可以在臨時分支中一直執行 rebase。那樣,如果你一不小心搞砸了你的 feature 分支歷史記錄,你可以多次檢查原始分支。例如: ``` git checkout feature git checkout -b temporary-branch git rebase -i master # [Clean up the history] git checkout master git merge temporary-branch ``` ## 總結 在你開始 rebasing 你的分支之前,這是所有你真正需要知道:如果您想要一個沒有不必要的乾淨的合併提交的線性歷史記錄,你應該爭取 `git rebase` 代替 `git merge` 整合來自另一個分支的改變。 另一方面,如果你想儲存你專案的完整歷史並且避免重寫公有提交的風險,你可以堅持使用 `git merge` 。任何一個選項都是完全有效的,至少現在你是有選擇性的利用 `git rebase` 的好處。 > 本文作者:Tim Pettersen, 翻譯:Queena > 原文連結:https://dzone.com/articles/merging-vs-rebasing > 譯文首發:http://didispace.com/git-merge-rebase-compare/ > 本文有spring4all技術翻譯組完成,更多國外前沿知識和乾貨好文,歡迎關注公眾號:後端面試那些