-
1. 起步
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協議
- 4.2 在伺服器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設伺服器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管服務
- 4.10 小結
-
5. 分散式 Git
-
A1. 附錄 A: Git 在其他環境
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小結
-
A2. 附錄 B: 在應用程式中嵌入 Git
-
A3. 附錄 C: Git 命令
3.6 Git 分支 - Rebase
Rebase
在 Git 中,將一個分支的更改整合到另一個分支主要有兩種方法:merge(合併)和 rebase(變基)。在本節中,您將瞭解 rebase 是什麼,如何進行 rebase,為什麼它是一個非常強大的工具,以及在什麼情況下不應使用它。
基本的 Rebase
如果您回顧一下 基本合併 中的一個早期示例,您會看到您分叉了工作並在兩個不同的分支上進行了提交。
正如我們之前討論過的,整合分支的最簡單方法是使用 merge 命令。它在兩個最新的分支快照(C3 和 C4)以及它們最近的共同祖先(C2)之間執行三方合併,建立一個新的快照(以及提交)。
然而,還有另一種方法:您可以獲取 C4 中引入的更改的補丁,然後將其重新應用到 C3 之上。在 Git 中,這稱為 rebasing(變基)。使用 rebase 命令,您可以獲取在一個分支上提交的所有更改,並將它們重新應用到另一個分支上。
對於這個例子,您將檢出 experiment 分支,然後像這樣將其 rebase 到 master 分支上:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
此操作的工作方式是:找到兩個分支(您當前所在的分支和您正在 rebase 到的分支)的共同祖先,獲取您所在分支的每個提交所引入的差異,將這些差異儲存到臨時檔案中,將當前分支重置到與您正在 rebase 到的分支相同的提交,最後依次應用每個更改。
C4 中引入的更改 rebase 到 C3 之上此時,您可以返回到 master 分支並執行快進合併。
$ git checkout master
$ git merge experiment
master 分支現在,C4' 指向的快照與 合併示例 中 C5 指向的快照完全相同。整合的最終產品沒有區別,但 rebase 使得歷史更加整潔。如果您檢查 rebase 後的分支的日誌,它看起來像一個線性的歷史:似乎所有工作都是按順序發生的,即使它們最初是並行發生的。
通常,您會這樣做以確保您的提交能夠乾淨地應用到遠端分支上——也許是您試圖貢獻但並未維護的專案。在這種情況下,您會在一個分支中完成工作,然後在準備提交補丁到主專案時,將您的工作 rebase 到 origin/master 之上。這樣,維護者就不必進行任何整合工作——只需要一個快進或乾淨的應用。
請注意,您最終得到的最後一個提交所指向的快照,無論是 rebase 後的最後一個 rebase 提交,還是合併後的最終合併提交,都是相同的快照——僅僅是歷史不同。Rebase 將工作線路上的更改按引入的順序重新應用到另一條工作線路上,而合併則將兩個端點合併在一起。
更有趣的 Rebase
您也可以讓 rebase 應用於非 rebase 目標分支。例如,考慮一個 從另一個主題分支分出的主題分支的歷史。您分出了一個主題分支(server)為您的專案新增伺服器端功能,並進行了一次提交。然後,您從該分支分出以進行客戶端更改(client),並進行了幾次提交。最後,您回到了 server 分支並又進行了幾次提交。
假設您決定將客戶端更改合併到主線以進行釋出,但希望在伺服器端更改經過進一步測試後再進行。您可以使用 git rebase 的 --onto 選項,將 client 分支中自與 server 分支分叉以來所做的更改(C8 和 C9)重新應用到 master 分支上。
$ git rebase --onto master server client
這基本上意味著:“獲取 client 分支,找出它與 server 分支分叉以來的補丁,並將這些補丁應用到 client 分支上,就好像它直接基於 master 分支一樣。”這有點複雜,但結果非常棒。
現在,您可以快進 master 分支(參見 快進 master 分支以包含 client 分支的更改)。
$ git checkout master
$ git merge client
master 分支以包含 client 分支的更改假設您也決定拉入 server 分支。您可以透過執行 git rebase <basebranch> <topicbranch> 來將 server 分支 rebase 到 master 分支上,而無需先檢出它——這會為您檢出主題分支(在此例中為 server),並將其 rebase 到基礎分支(master)之上。
$ git rebase master server
這會將您的 server 工作重新應用到您的 master 工作之上,如 將您的 server 分支 rebase 到您的 master 分支之上 所示。
server 分支 rebase 到您的 master 分支之上然後,您可以快進基礎分支(master)。
$ git checkout master
$ git merge server
您可以刪除 client 和 server 分支,因為所有工作都已整合,您不再需要它們了,這樣整個過程的歷史看起來就像 最終提交歷史。
$ git branch -d client
$ git branch -d server
Rebase 的危險
啊,但是 rebase 的美好並非沒有缺點,這些缺點可以歸結為一句話:
不要 rebase 那些已經存在於您的倉庫之外,並且可能有人在此基礎上進行了工作的提交。
如果您遵循這條準則,您將平安無事。如果您不遵循,人們會討厭您,您會被朋友和家人唾棄。
當您 rebase 東西時,您是在放棄現有的提交併建立新的、相似但不同的提交。如果您將提交推送到某個地方,其他人將其拉下來並在此基礎上進行了工作,然後您使用 git rebase 重寫了這些提交併再次推送,那麼您的協作者將不得不重新合併他們的工作,當您嘗試將他們的工作拉回到您的專案中時,情況會變得一團糟。
讓我們來看一個關於您公開的 rebase 工作可能導致問題的示例。假設您從一箇中心伺服器克隆,然後在上面進行了一些工作。您的提交歷史看起來是這樣的:
現在,其他人進行了更多工作,其中包括一次合併,並將這些工作推送到中心伺服器。您將其獲取下來,並將新的遠端分支合併到您的工作中,使您的歷史看起來像這樣:
接下來,推送了合併工作的人決定回過頭來 rebase 他們的工作;他們執行 git push --force 來覆蓋伺服器上的歷史。然後您從該伺服器獲取,下載新的提交。
現在您們都陷入了困境。如果您執行 git pull,您將建立一個合併提交,其中包含兩條歷史記錄,您的倉庫將看起來像這樣:
如果您在歷史看起來是這樣的情況下執行 git log,您會看到兩個具有相同作者、日期和訊息的提交,這將令人困惑。此外,如果您將此歷史推送到伺服器,您將把所有這些 rebase 後的提交重新引入中心伺服器,這可能會進一步混淆人們。可以安全地假設其他開發人員不希望 C4 和 C6 出現在歷史記錄中;這就是他們最初進行 rebase 的原因。
Rebase 時 Rebase
如果您確實遇到了類似這樣的情況,Git 有一些更神奇的功能可以幫助您。如果您的團隊中的某個人強制推送了覆蓋了您在其基礎上進行工作的更改,您的挑戰是弄清楚哪些是您的,哪些是他們重寫的。
原來,除了提交的 SHA-1 校驗和之外,Git 還計算了一個僅基於提交引入的補丁的校驗和。這被稱為“patch-id”。
如果您下載了被重寫的工作,並將其 rebase 到您合作伙伴的新提交之上,Git 通常能夠成功地找出哪些是您獨有的,並將它們重新應用到新分支之上。
例如,在前面的場景中,如果我們不再執行合併,而是處於 某人推送了 rebase 後的提交,放棄了您在其基礎上進行工作的提交 時,而是執行 git rebase teamone/master,Git 將會:
-
確定哪些工作是我們的分支獨有的(
C2、C3、C4、C6、C7)。 -
確定哪些不是合併提交(
C2、C3、C4)。 -
確定哪些尚未被重寫到目標分支(只有
C2和C3,因為C4與C4'是相同的補丁)。 -
將這些提交應用到
teamone/master的頂部。
因此,我們得到的不是 您將同一份工作再次合併到一個新的合併提交中 中看到的結果,而是類似 在強制推送的 rebase 工作之上進行 rebase 的情況。
這僅在您的合作伙伴所做的 C4 和 C4' 補丁幾乎完全相同時才有效。否則,rebase 將無法識別它們是重複的,並會新增另一個 C4 類的補丁(這很可能無法乾淨地應用,因為更改至少部分已經存在)。
您也可以透過執行 git pull --rebase 而不是普通的 git pull 來簡化這一點。或者,在這種情況下,您可以手動執行 git fetch,然後是 git rebase teamone/master。
如果您使用 git pull 並希望將 --rebase 設定為預設值,您可以使用類似 git config --global pull.rebase true 的命令設定 pull.rebase 配置值。
如果您只 rebase 那些從未離開過您計算機的提交,您將安然無恙。如果您 rebase 那些已經推送出去但沒有人在此基礎上進行過工作的提交,您也將安然無恙。如果您 rebase 那些已經公開推送的提交,並且人們可能在此基礎上進行了工作,那麼您可能會遇到一些令人沮喪的麻煩,並招致團隊成員的鄙視。
如果您或您的合作伙伴在某個時候發現有必要這樣做,請確保每個人都知道執行 git pull --rebase 以便在發生問題後儘量簡化處理。
Rebase 與 Merge
現在您已經看到了 rebase 和 merge 的實際應用,您可能想知道哪個更好。在回答這個問題之前,讓我們退後一步,談談歷史的意義。
關於這一點的一種觀點是,您的倉庫的提交歷史是實際發生的事情的記錄。它是一份歷史檔案,本身就很有價值,不應該被篡改。從這個角度來看,改變提交歷史幾乎是褻瀆神明的;您在撒謊關於實際發生的事情。那麼,為什麼會有混亂的合併提交系列呢?事情就是那樣發生的,倉庫應該為後代儲存下來。
相反的觀點是,提交歷史是您的專案是如何製作的故事。您不會出版一本書的第一稿,那又何必展示您混亂的工作呢?當您從事一個專案時,您可能需要一份記錄您所有失誤和死衚衕路徑的記錄,但當需要向世界展示您的工作時,您可能希望講述一個更連貫的故事,說明如何從 A 到 B。持此觀點的人使用 rebase 和 filter-branch 等工具在將提交合併到主線分支之前重寫他們的提交。他們使用 rebase 和 filter-branch 等工具,以最適合未來讀者的方式講述故事。
現在,關於 merge 還是 rebase 更好這個問題:希望您能看到這並非易事。Git 是一個強大的工具,允許您對歷史進行和進行許多操作,但每個團隊和每個專案都是不同的。現在您知道這兩者是如何工作的了,由您來決定哪一個最適合您的特定情況。
您可以獲得兩全其美:在推送之前 rebase 本地更改以清理您的工作,但永遠不要 rebase 任何您已經推送到某個地方的內容。