深入 git rebase
Git 的核心功能之一就是可以編輯歷史。與其它將歷史視為鐵律的版本控制系統不同,在 git 中我們可以根據需要更改歷史記錄。它為我們提供了許多強大的工具,允許我們像使用重構來維護良好的軟體設計一樣策劃良好的提交歷史。這些工具對新手甚至中級用戶來說可能有點難度,但指南將幫助解開 git-rebase 的神秘面紗。
一句警告:通常不建議改變公共、共享或穩定分支的歷史。編輯功能分支和個人復刻的分支是一個不錯的選擇,編輯你尚未推送的分支肯定是沒問題的。在編輯提交後,使用 git push -f 強制提交,會將更改推送到個人復刻或功能分支。
儘管有這樣嚴肅的警告,但值得一提的是,本指南中提到的都是非破壞性操作。在 git 中永久丟失數據實際上非常困難。指南末尾就介紹了在出錯時如何修復問題。
本指南由 sourcehut (駭客聯盟)提供。100% 開源且由 Mercurial 託管,持續集成,有郵件列表,且沒有 JavaScript!現在就試試吧!
配置沙箱
我們不想搞砸你原有的倉庫,因此本指南中我們將全程使用沙箱倉庫。從運行下面的命令開始:
如果遇到麻煩,只需要運行 rm -rf /tmp/rebase-sandbox 並在此運行上面的步驟即可重新開始。本指南的每個步驟都可以在新的沙箱中運行,因此沒有必要重新執行每項任務。
修改你的上一次提交
讓我們從簡單的修改開始:修復你最近的一次提交。我們在沙箱中添加一個文件(並且做了一個錯誤操作):
修復這個錯誤很容易。我們可以編輯文件並使用 --amend 提交,如下所示:
使用 -a 指令(即 git add)會自動暫存所有 git 已知的文件,而 --amend 會將更改壓縮到最近的提交中。保存並退出編輯器(如果你願意,也可以現在修改提交信息)。你可以通過運行 git show 查看這次修復的提交:
修復舊有提交
上面的修改僅適用最近的提交。如果你需要更正舊的提交又會怎樣么?我們首先對沙箱做出相應的設置:
看起來 greeting.txt 缺少了 "world" 單詞。我們來寫一個正常的提交來修復這點:
所以現在文件看起來是正確的,但我們的歷史還可以優化一下(使用一個新的提交來 "fixup" 最後一次提交)。為此,我們需要引入一個新的工具:互動式 rebase。我們將使用這種方式修改最後三個提交,運行 git rebase -i HEAD~3(-i 用與交互)。這會打開文本編輯器,如下所示:
這個是 rebase 執行計劃,通過編輯此文件,你可以指示 git 如何編輯歷史記錄。我已經將摘要信息修改為只與 rebase 指南中這一部分相關的摘要信息,但是你可以隨意閱覽文本編輯器中的完整摘要。
當我們保存並關閉編輯器時,git 將從歷史記錄中刪除這裡全部的提交記錄,然後依次執行每一行。默認情況下,它會找到每一次提交,從堆中將其取出並添加到分支中。如果我們根本沒有編輯這個文件,最終會回到開始的地方,按原有選擇每個提交。現在來使用一個我最喜歡的功能:fixup。編輯第三行,將 "pick" 修改為 "fixup",並立即將其移動到我們想要 "fix up" 為位置:
提示:我們同樣可以將其簡寫為 "f",來加快速度。
保存並退出編輯器(git 會運行這些命令)。我們可以檢查日誌來驗證結果:
壓縮多個提交至一個
你在工作時,可能會發現在你達成一個小的功能節點或修復先前提交中的錯誤時,編寫了大量有用的提交。但是,將這些提交壓縮("squash")在一起會更好,可以使你的工作在合併到 master 之前歷史更清晰。為此,我們將使用 "squash" 操作。我們先寫一堆提交(如果你想快一點,直接複製粘貼即可):
這裡會創建一個文件,說 "Hello world",做了很多次提交。我們開啟另一個互動式的 rebase 將它們合併到一起。注意,我們首先要檢出一個分支然後再嘗試此操作。正是這樣,由於我們檢出了一個新分支,所以允許使用 git rebase -i master來快速修改所有自分支檢出之後的提交。結果:
提示:你的本地 master 分支獨立於遠程 master 分支之外,且 git 將遠程分支存儲為 origin/master。結合這個技巧,使用 git rebase -i origin/master 來修改尚未合併到上游的所有提交是一種非常方便的方法。
我們要將這些更改全部壓縮到第一次提交中。要做到這一點,需要將除第一行之外的每個 "pick" 操作修改為 "squash",如下所示:
當你保存並關閉編輯器時,git 會處理一小會兒,然後再次打開編輯器來修改最終的提交消息。你會看到這樣:
默認這就是所有被壓縮後的提交消息的組合,但是保留這樣的消息並不是你想要的。不過,舊的提交消息在編寫新提交時可能有參考意義。
提示:你在上一節中了解到的 "fixup" 命令也可以用在這裡(但是它會丟棄壓縮提交的消息)。
現在我們刪除所有內容並用更好的提交消息替換,如下所示:
保存並退出編輯器,然後檢查你的 git 日誌,成功了!
在繼續之前,我們將作出的更改拉入 master 分支並清除這個新分支的痕迹。我們可以像使用 git merge 一樣使用 git rebase,但是這避免了合併提交操作:
除非是我們合併兩個不相關的歷史記錄,否則通常還是希望避免使用 git merge。如果你有兩個不同的分支,git merge 對於記錄它們於何時被合併是很有用的。在正常工作過程中,使用 rebase 通常更合適。
將一次提交拆分成多個
有時會遇到相反的問題(一次提交太多了)。我們來試著把它分開。這一次,編寫一點實際的代碼。從一個簡單的 C 程序開始(你仍然可以複製粘貼到 shell 來快速完成):
我們先做第一次提交。
之後,對程序做一些擴展:
提交之後,我們就為學習如何拆分提交做好準備了:
第一步是啟動一個互動式 rebase。我們使用 git rebase -i HEAD~2 來 rebase 兩次提交,下面給出這個 rebase 計劃:
將第二個提交命令由 "pick" 改為 "edit",然後保存並關閉編輯器。Git 會花費一會時間處理,然後顯示這個:
我們可以按照這些說明向提交中添加新的更改,不過這裡讓我們使用 git reset HEAD^ 來做一個 "soft reset"。如果你在這之後運行了 git status,你會看到git將最後一次提交的內容退還到編輯後還沒有stage的狀態,並將其加到工作樹中:
要拆分這個提交,我們需要做一個互動式 commit。這樣我們就可以有選擇地只修改工作樹中的特定更改。運行 git commit -p 來啟動此過程,你將看到一下提示:
Git 只給了一個 "大塊頭"(即單個的變更)來展示提交。不過這太多了,所以我們來使用 "s" 命令把這個大塊頭 "split" 成小的部分。
提示:如果你對其他選項比較好奇,按 "?" 來了解它們的描述。
這個大塊頭看起來也不錯,單一且變化自成一體。輸入 "y" 回復這個問題(並暫存這個 "大塊頭"),然後 "q" 退出互動式會話並繼續提交。你的編輯器突然提示並要求你輸出一個合適的提交信息。
保存並關閉你的編輯器,然後我們將進行第二次提交。我們可以打開另一個互動式 commit,但由於我們只想在這個提交中包含其餘的更改,只需要這麼做:
最後一個命令告訴 git,我們已經完成了對此提交的編輯,並繼續下一個 rebase 命令。就這樣!運行 git log 命令來看看你的勞動成果:
對提交重新排序
這個很容易了!我們從設置沙箱開始:
現在 git 日誌應該是這樣的:
顯然,這裡排序錯亂了。我們要針對過去的 3 個提交進行互動式 rebase,來解決這個問題。運行 git rebase -i HEAD~3 會出現這個 rebase 計劃:
這個修復過程很簡單:只需要按照你希望的提交順序重新排序這些行。看起來應該是這樣:
保存並關閉你的編輯器,git 會完成剩下的工作。注意,當你在實際工作中執行這個操作時,最終可能會發生衝突,點擊這裡查看衝突的幫助。
git pull --rebase
如果你一直在已經更新的上游分支創建提交,通常 git pull 會創建一個和並提交。這方便,git pull 的行為默認相當於:
還有另一個選擇,往往更有用且使歷史保持簡潔:git pull —rebase。與合併方式不同,這相當於:
合併的方式更簡單且更容易理解,但是如果你會使用 git rebase 的話,這種 rebase 方式則更容易達成你的目的。如果你願意,可以將將其設置為某些默認行為:
當你執行此操作時,技術上來說,你正在使用我們在下一節將討論的內容。所以我們來解釋一下這麼做的意義是什麼。
對 rebase 使用 rebase
諷刺的是,我使用最少的 git rebase 功能就是其中一個名為分支變基功能。比如你有一下分支:
事實證明,feature-2 並不依賴於 feature-1 中的任何變更,而是依賴於 E 提交,因此你可以將其 rebase 到 master。解決方式為:
非互動式 rebase 對所有相關提交("pick")都會執行默認操作,它會在 feature-2 重複這些在 master 頂部而非 feature-1 中的提交。你的提交歷史現在看起來像這樣:
解決衝突
解決合併衝突的細節超出了本指南的範圍(留意之後針對此功能的指南)。假設你熟悉解決一般衝突,下面是使用 rebase 的細節。
有時,在執行 rebase 時會遇到合併衝突,你可以像處理其他衝突一樣處理 rebase。Git 會在受影響的文件中設置衝突標記,git status 可以顯示這些待解決的問題,並且你可以將解決衝突後的文件使用 git add 或 git rm 標記。然而,在 git rebase 的背景下,有兩個選擇你要注意。
第一個就是如何徹底解決衝突。不同於 git commit,你將要解決的是由 git merge 造成的衝突,重新 rebase 的適用命令是 git rebase --continue。然而,這裡還有其他的可用的選擇:git rebase --skip。它會跳過你正在處理的提交,並將其從 rebase 中移除。在互動式 rebase 中,當 git 沒有意識到提交是從其他分支來出來的,且我們當前分支已有一個更新的版本,這是很常見的。
來幫幫忙!我把這個搞砸了!
毫無疑問,rebase 操作有時可能會很困難。如果你犯一個錯誤並因此丟失了一個你需要的提交,而這裡有一個git reflog 命令可以保存當天的歷史。運行這個命令你會看到你所有變更操作的 ref,或者是分支、標籤的引用。每一行都顯示了引用的指向,並且一旦你認為提交丟失了,就可以使用 git cherry-pick,git checkout,git show或其他操作來處理。
英文原文:https://git-rebase.io/ 譯者:敦偉
※如何知道要測試什麼?
※在Python中如何使用sorted()和sort()函數
TAG:Python部落 |