Git Rebase手冊(cè) – Rebase權(quán)威指南
事實(shí)是,如果您了解它的實(shí)際用途,就會(huì)git rebase發(fā)現(xiàn)它是一個(gè)非常優(yōu)雅且簡(jiǎn)單的工具,可以在 Git 中實(shí)現(xiàn)許多不同的事情。
在之前的文章中,您了解了Git 差異是什么、合并是什么以及Git 如何解決合并沖突。在這篇文章中,您將了解 Git rebase 是什么,為什么它與 merge 不同,以及如何自信地進(jìn)行 rebase ????
(更|多優(yōu)質(zhì)內(nèi)|容:java567 點(diǎn) c0m)
開(kāi)始之前的注意事項(xiàng)
我還制作了一個(gè)涵蓋這篇文章內(nèi)容的視頻。如果您想在閱讀的同時(shí)觀看,可以在這里找到。
如果您想使用我使用的存儲(chǔ)庫(kù)并親自嘗試命令,您可以在此處獲取存儲(chǔ)庫(kù)。
我正在寫(xiě)一本關(guān)于 Git 的書(shū)!您有興趣閱讀初始版本并提供反饋嗎?
好的,你準(zhǔn)備好了嗎?
簡(jiǎn)短回顧 - 什么是 Git Merge???
在幕后,git rebase和git merge是非常非常不同的東西。那么為什么人們總是比較他們呢?
原因在于它們的用途。使用 Git 時(shí),我們通常在不同的分支中工作并對(duì)這些分支引入更改。
在之前的教程中,我舉了一個(gè)例子,約翰和保羅(披頭士樂(lè)隊(duì)的成員)正在共同創(chuàng)作一首新歌。他們從main分支開(kāi)始,然后各自發(fā)散,修改歌詞并提交自己的更改。
然后,兩人想要集成他們的更改,這是使用 Git 時(shí)經(jīng)常發(fā)生的事情。
一段不同的歷史——paul_branch并且john_branch背離了main(來(lái)源:Brief)
有兩種主要方法可以集成 Git 中不同分支中引入的更改,或者換句話說(shuō),不同的提交和提交歷史記錄。這些是合并和變基。
在之前的教程中,我們已經(jīng)git merge非常了解了。我們看到,在執(zhí)行合并時(shí),我們創(chuàng)建一個(gè)合并提交- 該提交的內(nèi)容是兩個(gè)分支的組合,并且它還有兩個(gè)父級(jí),每個(gè)分支一個(gè)。
因此,假設(shè)您位于分支上john_branch(假設(shè)上圖中描述的歷史記錄),然后運(yùn)行g(shù)it merge paul_branch. 您將進(jìn)入這種狀態(tài) – 在 上john_branch,有兩個(gè)父母的新提交。第一個(gè)是執(zhí)行合并之前指向的john_branch分支上的提交,在本例中為“Commit 6”。HEAD第二個(gè)是paul_branch“Commit 9”指向的提交。
運(yùn)行結(jié)果git merge paul_branch:有兩個(gè)父級(jí)的新合并提交(來(lái)源:Brief)
再看看歷史圖表:您創(chuàng)建了一個(gè)分歧的歷史。您實(shí)際上可以看到它在哪里分支以及在哪里再次合并。
因此,在使用時(shí)git merge,您不會(huì)重寫(xiě)歷史記錄 - 而是向現(xiàn)有歷史記錄添加提交。具體來(lái)說(shuō),是創(chuàng)建分歧歷史記錄的提交。
與 有何git rebase不同git merge???
使用時(shí)git rebase,會(huì)發(fā)生不同的情況。??
讓我們從大局開(kāi)始:如果您在 上paul_branch,并使用git rebase john_branch,Git 將轉(zhuǎn)到 John 分支和 Paul 分支的共同祖先。然后,它采用 Paul 分支上的提交中引入的補(bǔ)丁,并將這些更改應(yīng)用到 John 的分支。
因此,在這里,您通常rebase會(huì)獲取在一個(gè)分支(Paul 的分支)上提交的更改,然后在另一個(gè)分支上重播它們john_branch。
運(yùn)行結(jié)果:上面git rebase john_branch的提交被“重放” (來(lái)源:Brief)paul_branch``john_branch
等等,這是什么意思???
我們現(xiàn)在將一點(diǎn)一點(diǎn)地了解這一點(diǎn),以確保您完全了解幕后發(fā)生的事情??
cherry-pick作為 Rebase 的基礎(chǔ)
將變基視為執(zhí)行是有用的git cherry-pick- 一個(gè)命令接受一次提交,通過(guò)計(jì)算父級(jí)提交和提交本身之間的差異來(lái)計(jì)算此提交引入的補(bǔ)丁,然后cherry-pick“重放”此差異。
讓我們手動(dòng)執(zhí)行此操作。
如果我們通過(guò)執(zhí)行以下命令來(lái)查看“Commit 5”引入的差異git diff main <SHA_OF_COMMIT_5>:
運(yùn)行g(shù)it diff觀察“Commit 5”引入的補(bǔ)?。▉?lái)源:Brief)
(如果您想使用我使用的存儲(chǔ)庫(kù)并親自嘗試命令,您可以在此處獲取存儲(chǔ)庫(kù))。
您可以看到,在此提交中,約翰開(kāi)始創(chuàng)作一首名為“Lucy in the Sky with Diamonds”的歌曲:
git diff“Commit 5”引入的補(bǔ)丁的輸出(來(lái)源: Brief)
提醒一下,您還可以使用以下命令git show獲得相同的輸出:
?git show <SHA_OF_COMMIT_5>
現(xiàn)在,如果您cherry-pick進(jìn)行此提交,您將在活動(dòng)分支上專門引入此更改。切換到main第一個(gè):
git checkout main(或git switch main)
并創(chuàng)建另一個(gè)分支,只是為了清楚起見(jiàn):
git checkout -b my_branch(或git switch -c my_branch)
創(chuàng)建my_branch分支main(來(lái)源:Brief)
而cherry-pick這個(gè)提交:
?git cherry-pick <SHA_OF_COMMIT_5>
用于cherry-pick將“Commit 5”中引入的更改應(yīng)用到main(來(lái)源:Brief)
考慮日志( 的輸出git lol):
的輸出git lol(來(lái)源:Brief)
(git lol是我添加到 Git 中的別名,以便以圖形方式直觀地查看歷史記錄。您可以在此處找到它)。
看來(lái)您復(fù)制粘貼了“Commit 5”。請(qǐng)記住,即使它具有相同的提交消息,并引入相同的更改,甚至在本例中指向與原始“Commit 5”相同的樹(shù)對(duì)象 - 它仍然是一個(gè)不同的提交對(duì)象,因?yàn)樗鞘褂貌煌臅r(shí)間戳。
查看更改,使用git show HEAD:
的輸出git show HEAD(來(lái)源:Brief)
它們與“Commit 5”相同。
當(dāng)然,如果您查看該文件(例如,通過(guò)使用nano lucy_in_the_sky_with_diamonds.md),它將處于與原始“Commit 5”之后相同的狀態(tài)。
涼爽的!??
好的,您現(xiàn)在可以刪除新分支,這樣它就不會(huì)每次都出現(xiàn)在您的歷史記錄中:
?git checkout main
?git branch -D my_branch
Beyond cherry-pick– 如何使用git rebase
您可以將git rebase其視為一種依次執(zhí)行多個(gè)cherry-pick操作的方法,即“重播”多個(gè)提交。這不是您可以做的唯一事情rebase,但它是我們解釋的一個(gè)很好的起點(diǎn)。
是時(shí)候一起玩了git rebase!????????
之前,你paul_branch并入john_branch. 如果您基于重新建立 paul_branch基礎(chǔ), ?會(huì)發(fā)生什么john_branch?你會(huì)得到一段非常不同的歷史。
從本質(zhì)上講,我們似乎采用了 上的提交中引入的更改paul_branch,并在 上重播了它們john_branch。結(jié)果將是一個(gè)線性歷史。
為了理解這個(gè)過(guò)程,我將提供高層次的視圖,然后深入研究每個(gè)步驟。將一個(gè)分支變基到另一分支之上的過(guò)程如下:
尋找共同祖先。
確定要“重播”的提交。
對(duì)于每次提交X,計(jì)算diff(parent(X), X)并將其存儲(chǔ)為patch(X).
遷往HEAD新基地。
將生成的補(bǔ)丁按順序應(yīng)用到目標(biāo)分支上。每次,創(chuàng)建一個(gè)具有新?tīng)顟B(tài)的新提交對(duì)象。
使用與現(xiàn)有變更集相同的變更集進(jìn)行新提交的過(guò)程也稱為“重放”這些提交,這是我們已經(jīng)使用過(guò)的術(shù)語(yǔ)。
是時(shí)候?qū)嵺` Rebase 了????
從Paul的分支開(kāi)始:
?git checkout paul_branch
這是歷史:
執(zhí)行前提交歷史記錄git rebase(來(lái)源:Brief)
現(xiàn)在,到了令人興奮的部分:
?git rebase john_branch
并觀察歷史:
rebase后的歷史(來(lái)源:Brief)
(是我在視頻中g(shù)g介紹的外部工具的別名)。
因此,隨著git merge你被添加到歷史中,隨著git rebase你重寫(xiě)歷史。您創(chuàng)建新的提交對(duì)象。此外,結(jié)果是線性歷史圖,而不是發(fā)散圖。
rebase后的歷史(來(lái)源:Brief)
本質(zhì)上,我們“復(fù)制”了“Commit 4”之后引入的提交paul_branch,并將它們“粘貼”到j(luò)ohn_branch.
該命令稱為“rebase”,因?yàn)樗牧诉\(yùn)行它的分支的基本提交。也就是說(shuō),在您的情況下,在運(yùn)行之前git rebase, 的基礎(chǔ)paul_branch是“Commit 4” - 因?yàn)檫@是分支“誕生”的地方(來(lái)自main)。通過(guò)rebase,您要求 Git 給它另一個(gè)基礎(chǔ) - 也就是說(shuō),假裝它是從“Commit 6”誕生的。
為此,Git 采用了以前的“Commit 7”,并將此提交中引入的更改“重播”到“Commit 6”上,然后創(chuàng)建了一個(gè)新的提交對(duì)象。這個(gè)對(duì)象與原來(lái)的“Commit 7”有三個(gè)方面的不同:
它有不同的時(shí)間戳。
它有一個(gè)不同的父提交 - “Commit 6”而不是“Commit 4”。
它指向的樹(shù)對(duì)象不同 - 因?yàn)楦氖且氲健癈ommit 6”指向的樹(shù),而不是“Commit 4”指向的樹(shù)。
請(qǐng)注意此處的最后一次提交“Commit 9'”。它所代表的快照(即它指向的樹(shù))與通過(guò)合并兩個(gè)分支得到的樹(shù)完全相同。Git 存儲(chǔ)庫(kù)中文件的狀態(tài)與您使用git merge. 只是歷史不同,當(dāng)然還有提交對(duì)象。
現(xiàn)在,您可以簡(jiǎn)單地使用:
?git checkout main
?git merge paul_branch
嗯...如果您運(yùn)行最后一條命令會(huì)發(fā)生什么??? 檢查后再次考慮提交歷史記錄main:
變基和檢出后的歷史main(來(lái)源:Brief)
main合并和意味著什么paul_branch?
事實(shí)上,Git 可以簡(jiǎn)單地執(zhí)行快進(jìn)合并,因?yàn)闅v史記錄是完全線性的(如果您需要有關(guān)快進(jìn)合并的提醒,請(qǐng)查看這篇文章)。結(jié)果,main現(xiàn)在paul_branch指向相同的提交:
快進(jìn)合并的結(jié)果(來(lái)源:Brief)
Git 中的高級(jí)變基????
現(xiàn)在您已經(jīng)了解了 rebase 的基礎(chǔ)知識(shí),是時(shí)候考慮更高級(jí)的情況了,在這些情況下,命令的附加開(kāi)關(guān)和參數(shù)rebase將派上用場(chǎng)。
在前面的示例中,當(dāng)您僅表示rebase(沒(méi)有附加開(kāi)關(guān))時(shí),Git 會(huì)重播從共同祖先到當(dāng)前分支尖端的所有提交。
但 rebase 是一種超級(jí)力量,它是一個(gè)全能的命令,能夠……嗯,重寫(xiě)歷史。如果您想修改歷史記錄以使其成為您自己的歷史記錄,它會(huì)派上用場(chǎng)。
通過(guò)再次指向“Commit 4”來(lái)撤消上次合并main:
?git reset -–hard <ORIGINAL_COMMIT 4>
“撤消”最后一次合并操作(來(lái)源:Brief)
并使用以下命令撤消變基:
?git checkout paul_branch
?git reset -–hard <ORIGINAL_COMMIT 9>
“撤消”變基操作(來(lái)源:Brief)
請(qǐng)注意,您獲得的歷史記錄與以前的歷史記錄完全相同:
可視化“撤消”變基操作后的歷史記錄(來(lái)源:Brief)
再次需要明確的是,當(dāng)無(wú)法從當(dāng)前 .commit 9 訪問(wèn)時(shí),“Commit 9”并不會(huì)消失HEAD。相反,它仍然存儲(chǔ)在對(duì)象數(shù)據(jù)庫(kù)中。當(dāng)您git reset現(xiàn)在更改HEAD為指向此提交時(shí),您能夠檢索它及其父提交,因?yàn)樗鼈円泊鎯?chǔ)在數(shù)據(jù)庫(kù)中。很酷吧???
好的,快速查看 Paul 引入的更改:
?git show HEAD
git show HEAD顯示“Commit 9”引入的補(bǔ)?。▉?lái)源:Brief)
在提交圖中繼續(xù)向后移動(dòng):
?git show HEAD~
git show HEAD~(同git show HEAD~1)顯示“Commit 8”引入的補(bǔ)丁(來(lái)源:Brief)
并進(jìn)一步承諾:
?git show HEAD~2
git show HEAD~2顯示“Commit 7”引入的補(bǔ)?。▉?lái)源:Brief)
所以,這些改變很好,但也許保羅不想要這樣的歷史。相反,他希望看起來(lái)好像他將“Commit 7”和“Commit 8”中的更改作為單個(gè)提交引入。
為此,您可以使用交互式變基。為此,我們將-i(或--interactive) 開(kāi)關(guān)添加到rebase命令中:
?git rebase -i <SHA_OF_COMMIT_4>
或者,由于main指向“Commit 4”,我們可以簡(jiǎn)單地運(yùn)行:
?git rebase -i main
通過(guò)運(yùn)行此命令,您可以告訴 Git 使用新的基礎(chǔ)“Commit 4”。因此,您要求 Git 返回“Commit 4”之后引入的所有提交,并且可以從 current 訪問(wèn)這些提交HEAD,并重播這些提交。
對(duì)于重播的每個(gè)提交,Git 都會(huì)詢問(wèn)我們想用它做什么:
git rebase -i main提示您選擇每次提交要執(zhí)行的操作(來(lái)源:Brief)
在這種情況下,將提交視為補(bǔ)丁是很有用的。也就是說(shuō),“Commit 7”如““Commit 7”在其父級(jí)之上引入的補(bǔ)丁”一樣。
一種選擇是使用pick. 這是默認(rèn)行為,它告訴 Git 重放此提交中引入的更改。在這種情況下,如果您保持原樣 - 以及pick所有提交 - 您將獲得相同的歷史記錄,并且 Git 甚至不會(huì)創(chuàng)建新的提交對(duì)象。
另一種選擇是squash。壓縮的提交會(huì)將其內(nèi)容“折疊”到其前面的提交的內(nèi)容中。因此,在我們的例子中,Paul 希望將“Commit 8”壓縮為“Commit 7”:
將“Commit 8”壓縮為“Commit 7”(來(lái)源:Brief)
如您所見(jiàn),git rebase -i提供了其他選項(xiàng),但我們不會(huì)在本文中討論所有選項(xiàng)。如果您允許運(yùn)行變基,系統(tǒng)將提示您為新創(chuàng)建的提交選擇一條提交消息(即引入“Commit 7”和“Commit 8”更改的消息):
提供提交消息:(Commits 7+8來(lái)源:Brief)
看看歷史:
交互式rebase之后的歷史(來(lái)源:Brief)
正是我們想要的!我們有paul_branch“Commit 9”(當(dāng)然,它是與原始“Commit 9”不同的對(duì)象)。這里指向“Commits 7+8”,這是一個(gè)單一的提交,同時(shí)引入了原始“Commit 7”和原始“Commit 8”的更改。該提交的父級(jí)是“Commit 4”,main指向哪里。你有john_branch。
交互式變基后的歷史 - 可視化(來(lái)源:Brief)
哦,哇,這不是很酷嗎???
git rebase讓您可以無(wú)限制地控制任何分支的形狀。您可以使用它來(lái)重新排序提交,或刪除不正確的更改,或回顧修改更改?;蛘?,您也許可以將分支的基礎(chǔ)移動(dòng)到另一個(gè)提交(您希望的任何提交)。
如何使用--onto開(kāi)關(guān)git rebase
讓我們?cè)倏紤]一個(gè)例子。再次進(jìn)入main:
?git checkout main
并刪除指向的指針paul_branch,john_branch這樣您就不會(huì)再在提交圖中看到它們:
?git branch -D paul_branch
?git branch -D john_branch
現(xiàn)在分支main到一個(gè)新分支:
?git checkout -b new_branch
創(chuàng)造new_branch不同于main(來(lái)源:Brief)
new_branch與此不同的干凈歷史main(來(lái)源:Brief)
現(xiàn)在,在此處添加一些更改并提交它們:
?nano code.py
new_branch添加該功能code.py(來(lái)源:Brief)
?git add code.py
?git commit -m "Commit 10"
回到main:
?git checkout main
并引入另一個(gè)變化:
在文件開(kāi)頭添加了文檔字符串(來(lái)源:Brief)
是時(shí)候準(zhǔn)備并提交這些更改了:
?git add code.py
?git commit -m "Commit 11"
還有另一個(gè)變化:
添加@Author到文檔字符串(來(lái)源:Brief)
也提交此更改:
?git add code.py
?git commit -m "Commit 12"
哦等等,現(xiàn)在我意識(shí)到我希望您將“Commit 11”中引入的更改作為new_branch. 啊。你能做什么???
回顧一下歷史:
引入“Commit 12”后的歷史(來(lái)源:Brief)
我想要的是,我不希望“Commit 10”僅駐留在分支上main,而是希望它同時(shí)位于main分支和new_branch. 從視覺(jué)上看,我想將它移到圖表中:
從視覺(jué)上看,我希望你“推動(dòng)”“Commit 10”(來(lái)源:Brief)
你能看到我要去哪里嗎???
嗯,正如我們所理解的,rebase 允許我們基本上重放在“Commit 10”中引入的更改new_branch,就好像它們最初是在“Commit 11”而不是“Commit 4”上進(jìn)行的。
為此,您可以使用 的其他參數(shù)git rebase。您會(huì)告訴 Git,您想要獲取main和的共同祖先new_branch(即“Commit 4”)之間引入的所有歷史記錄,并將該歷史記錄的新基礎(chǔ)設(shè)為“Commit 11”。為此,請(qǐng)使用:
?git rebase -–onto <SHA_OF_COMMIT_11> main new_branch
rebase前后的歷史,“Commit 10”已被“推送”(來(lái)源:Brief)
看看我們美麗的歷史!??
rebase前后的歷史,“Commit 10”已被“推送”(來(lái)源:Brief)
讓我們考慮另一個(gè)案例。
假設(shè)我開(kāi)始在一個(gè)分支上工作,并且錯(cuò)誤地從 開(kāi)始工作feature_branch_1,而不是從 開(kāi)始工作main。
因此,要模擬這一點(diǎn),請(qǐng)創(chuàng)建feature_branch_1:
?git checkout main
?git checkout -b feature_branch_1
并擦除,new_branch這樣您就不會(huì)再在圖表中看到它:
?git branch -D new_branch
創(chuàng)建一個(gè)簡(jiǎn)單的 Python 文件,名為1.py:
一個(gè)新文件,1.py,包含print('Hello world!')(來(lái)源:Brief)
暫存并提交此文件:
?git add 1.py
?git commit -m ?"Commit 13"
現(xiàn)在(錯(cuò)誤地)從以下分支出來(lái)feature_branch_1:
?git checkout -b feature_branch_2
并創(chuàng)建另一個(gè)文件2.py:
創(chuàng)建2.py(來(lái)源:Brief)
也暫存并提交該文件:
?git add 2.py
?git commit -m ?"Commit 14"
并引入更多代碼2.py:
修改2.py(來(lái)源:Brief)
也暫存并提交這些更改:
?git add 2.py
?git commit -m ?"Commit 15"
到目前為止你應(yīng)該有這樣的歷史:
引入“Commit 15”后的歷史(來(lái)源:Brief)
返回feature_branch_1并編輯1.py:
?git checkout feature_branch_1
修改1.py(來(lái)源:Brief)
現(xiàn)在暫存并提交:
?git add 1.py
?git commit -m ?"Commit 16"
您的歷史記錄應(yīng)該如下所示:
引入“Commit 16”后的歷史(來(lái)源:Brief)
說(shuō)現(xiàn)在你意識(shí)到你犯了一個(gè)錯(cuò)誤。你實(shí)際上想feature_branch_2從樹(shù)枝中誕生main,而不是從……中誕生feature_branch_1。
你怎樣才能做到這一點(diǎn)???
--onto嘗試根據(jù)歷史圖以及您對(duì)命令標(biāo)志的了解來(lái)思考它rebase。
好吧,您想要將 上的第一個(gè)提交feature_branch_2(即“Commit 14”)的父級(jí)“替換”到main分支頂部(在本例中為“Commit 12”),而不是在分支頂部feature_branch_1(在本例中為“”)提交 13 英寸。同樣,您將創(chuàng)建一個(gè)新的基礎(chǔ),這次 是為了第一次提交feature_branch_2.
您想要移動(dòng)“Commit 14”和“Commit 15”(來(lái)源:Brief)
你會(huì)怎么做?
首先,切換到feature_branch_2:
?git checkout feature_branch_2
現(xiàn)在您可以使用:
?git rebase -–onto main <SHA_OF_COMMIT_13>
因此,您feature_branch_2基于main而不是feature_branch_1:
執(zhí)行rebase后的提交歷史(來(lái)源:Brief)
該命令的語(yǔ)法是:
?git rebase --onto <new_parent> <old_parent>
如何在單個(gè)分支上變基
git rebase您還可以在查看單個(gè)分支的歷史記錄時(shí)使用。
讓我們看看你是否可以在這里幫助我。
假設(shè)我工作feature_branch_2并專門編輯了該文件code.py。我首先將所有字符串更改為用雙引號(hào)而不是單引號(hào)括起來(lái):
更改'為"in code.py(來(lái)源:Brief)
然后,我上演并承諾:
?git add code.py
?git commit -m "Commit 17"
然后我決定在文件的開(kāi)頭添加一個(gè)新函數(shù):
添加功能another_feature(來(lái)源:Brief)
我再次上演并承諾:
?git add code.py
?git commit -m "Commit 18"
現(xiàn)在我意識(shí)到我實(shí)際上忘記將單引號(hào)更改為雙引號(hào)main(正如您可能已經(jīng)注意到的那樣),所以我也這樣做了:
改成(來(lái)源'main':簡(jiǎn)報(bào))"main"
當(dāng)然,我策劃并承諾了這一改變:
?git add code.py
?git commit -m "Commit 19"
現(xiàn)在,回顧一下歷史:
引入“Commit 19”后的提交歷史(來(lái)源:Brief)
這不太好,不是嗎?我的意思是,我有兩個(gè)彼此相關(guān)的提交,“Commit 17”和“Commit 19”(將's 變成"s),但它們被不相關(guān)的“Commit 18”(我在其中添加了一個(gè)新函數(shù))分開(kāi)。我們可以做什么??? 你能幫我嗎?
直覺(jué)上,我想在這里編輯歷史記錄:
這些是我要編輯的提交(來(lái)源:Brief)
那么,你會(huì)怎么做?
你是對(duì)的!????
我可以在“Commit 15”之上將歷史記錄從“Commit 17”重新設(shè)置為“Commit 19”。要做到這一點(diǎn):
?git rebase --interactive --onto <SHA_OF_COMMIT_15> <SHA_OF_COMMIT_15>
請(qǐng)注意,我指定“Commit 15”作為提交范圍的開(kāi)頭,不包括此提交。而且我不需要明確指定HEAD為最后一個(gè)參數(shù)。
rebase --onto在單個(gè)分支上使用(來(lái)源: Brief)
按照您的建議并運(yùn)行rebase命令后(謝謝!??)我得到以下屏幕:
交互式變基(來(lái)源:Brief)
那么我該怎么辦呢?我想將“Commit 19”放在“Commit 18”之前,因此它位于“Commit 17”之后。我可以更進(jìn)一步,將它們壓在一起,如下所示:
交互式變基 - 更改提交和壓縮的順序(來(lái)源:Brief)
現(xiàn)在,當(dāng)我收到提交消息提示時(shí),我可以提供消息“Commit 17+19”:
提供提交消息(來(lái)源:Brief)
現(xiàn)在,看看我們美麗的歷史:
由此產(chǎn)生的歷史(來(lái)源:Brief)
再次感謝!????
更多變基用例+更多實(shí)踐
到目前為止,我希望您對(duì) rebase 的語(yǔ)法感到滿意。真正理解它的最好方法是考慮各種情況并自己找出解決方法。
對(duì)于即將到來(lái)的用例,我強(qiáng)烈建議您在介紹完每個(gè)用例后停止閱讀,然后嘗試自己解決它。
如何排除提交
假設(shè)您在另一個(gè)存儲(chǔ)庫(kù)上有此歷史記錄:
另一個(gè)提交歷史(來(lái)源:Brief)
在使用它之前,將標(biāo)簽存儲(chǔ)到“Commit F”,以便稍后可以返回:
?git tag original_commit_f
現(xiàn)在,您實(shí)際上不希望包含“Commit C”和“Commit D”中的更改。您可以像以前一樣使用交互式變基并刪除它們的更改。或者,可以再次使用git rebase -–onto。您將如何使用--onto來(lái)“刪除”這兩個(gè)提交?
您可以HEAD在“Commit B”之上進(jìn)行變基,其中舊的父級(jí)實(shí)際上是“Commit D”,現(xiàn)在它應(yīng)該是“Commit B”。再回顧一下歷史:
再次回顧歷史(來(lái)源:Brief)
變基使“Commit B”成為“Commit E”的基礎(chǔ),意味著“移動(dòng)”“Commit E”和“Commit F”,并給它們另一個(gè)基礎(chǔ)—— “ Commit B”。你能自己想出這個(gè)命令嗎?
?git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D> HEAD
請(qǐng)注意,使用上面的語(yǔ)法不會(huì)移動(dòng)main到指向新的提交,因此結(jié)果是“分離” HEAD。如果您使用gg或其他顯示可從分支訪問(wèn)的歷史記錄的工具,它可能會(huì)讓您感到困惑:
變基并--onto產(chǎn)生分離結(jié)果HEAD(來(lái)源:Brief)
但如果您只是使用git log(或我的別名git lol),您將看到所需的歷史記錄:
由此產(chǎn)生的歷史(來(lái)源:Brief)
我不了解你,但這些事情讓我真的很高興。????
順便說(shuō)一句,您可以省略HEAD上一個(gè)命令,因?yàn)檫@是第三個(gè)參數(shù)的默認(rèn)值。所以只需使用:
?git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D>
會(huì)有同樣的效果。最后一個(gè)參數(shù)實(shí)際上告訴 Git 當(dāng)前的 rebase 提交序列的結(jié)尾在哪里。git rebase --onto所以帶有三個(gè)參數(shù)的語(yǔ)法是:
?git rebase --onto <new_parent> <old_parent> <until>
如何跨分支移動(dòng)提交
假設(shè)我們得到了與之前相同的歷史記錄:
?git checkout original_commit_f
現(xiàn)在我只想要“提交 E”位于基于“提交 B”的分支上。也就是說(shuō),我想要一個(gè)新分支,從“Commit B”分支,只有“Commit E”。
當(dāng)前的歷史,考慮“Commit E”(來(lái)源:Brief)
那么,這對(duì)于 rebase 來(lái)說(shuō)意味著什么呢?考慮上面的圖片。我應(yīng)該重新設(shè)定哪些提交(或哪些提交),以及哪個(gè)提交將成為新的基礎(chǔ)?
我知道我可以在這里依靠你??
我想要的是僅采用“Commit E”,并且僅此提交,并將其基礎(chǔ)更改為“Commit B”。換句話說(shuō),將“提交 E”中引入的更改重播到“提交 B”上。
你能將該邏輯應(yīng)用到 的語(yǔ)法中嗎git rebase?
這是(為了簡(jiǎn)潔,這次我寫(xiě)<COMMIT_B>的是<SHA_OF_COMMIT_B>):
?git rebase –-onto <COMMIT_B> <COMMIT_D> <COMMIT_E>
現(xiàn)在歷史看起來(lái)是這樣的:
rebase后的歷史(來(lái)源:Brief)
驚人的!
關(guān)于沖突的注意事項(xiàng)
請(qǐng)注意,執(zhí)行變基時(shí),您可能會(huì)像合并時(shí)一樣遇到?jīng)_突。您可能會(huì)遇到?jīng)_突,因?yàn)樵谧兓鶗r(shí),您試圖在不同的基礎(chǔ)上應(yīng)用補(bǔ)丁,也許補(bǔ)丁不適用。
例如,再次考慮以前的存儲(chǔ)庫(kù),具體來(lái)說(shuō),考慮“Commit 12”中引入的更改,由 指向main:
?git show main
“Commit 12”中引入的補(bǔ)?。▉?lái)源:Brief)
我已經(jīng)在上一篇文章git diff中詳細(xì)介紹了 的格式,但作為一個(gè)快速提醒,此提交指示 Git 在兩行上下文之后添加一行:
?```
?This is a sample file
在這三行上下文之前:
?```
?def new_feature():
? ?print('new feature')
假設(shè)您正在嘗試將“Commit 12”重新設(shè)置為另一個(gè)提交。如果由于某種原因,這些上下文行并不像您要變基到的提交的補(bǔ)丁中那樣存在,那么您將遇到?jīng)_突。要了解有關(guān)沖突以及如何解決沖突的更多信息,請(qǐng)參閱本指南。
縮小大局
比較變基和合并(來(lái)源:Brief)
git merge在本指南的開(kāi)頭,我首先提到和 之間的相似之處git rebase:兩者都用于整合不同歷史中引入的變化。
但是,正如您現(xiàn)在所知,它們的運(yùn)作方式非常不同。合并會(huì)產(chǎn)生發(fā)散的歷史記錄,而變基則會(huì)產(chǎn)生線性歷史記錄。這兩種情況都可能發(fā)生沖突。上表中還有一列需要密切關(guān)注。
現(xiàn)在您知道什么是“Git rebase”,以及如何使用交互式 rebase,或者rebase --onto,正如我希望您同意的那樣,git rebase它是一個(gè)超級(jí)強(qiáng)大的工具。然而,與合并相比,它有一個(gè)巨大的缺點(diǎn)。
Git rebase 改變了歷史。
這意味著您不應(yīng)該對(duì)存儲(chǔ)庫(kù)本地副本之外存在的提交進(jìn)行變基,而其他人可能已經(jīng)基于這些提交進(jìn)行了提交。
換句話說(shuō),如果唯一有問(wèn)題的提交是您在本地創(chuàng)建的提交 - 繼續(xù),使用 rebase,盡情發(fā)揮。
但是,如果提交已被推送,這可能會(huì)導(dǎo)致一個(gè)巨大的問(wèn)題 - 因?yàn)槠渌丝赡軙?huì)依賴這些提交,而您稍后會(huì)覆蓋這些提交,然后您和他們將擁有不同版本的存儲(chǔ)庫(kù)。
正如我們所看到的,這與merge不修改歷史不同。
例如,考慮我們重新設(shè)置基礎(chǔ)并導(dǎo)致此歷史記錄的最后一個(gè)案例:
rebase后的歷史(來(lái)源:Brief)
現(xiàn)在,假設(shè)我已經(jīng)將此分支推送到遠(yuǎn)程。在我推送分支后,另一位開(kāi)發(fā)人員將其拉出并從“Commit C”分支出來(lái)。另一位開(kāi)發(fā)人員不知道與此同時(shí),我正在本地重新調(diào)整我的分支,并稍后再次推送它。
這會(huì)導(dǎo)致不一致:其他開(kāi)發(fā)人員所使用的提交在我的存儲(chǔ)庫(kù)副本上不再可用。
我不會(huì)在本指南中詳細(xì)說(shuō)明這到底是什么原因,因?yàn)槲业闹饕畔⑹悄^對(duì)應(yīng)該避免這種情況。如果您對(duì)實(shí)際發(fā)生的情況感興趣,我將在下面留下一個(gè)有用資源的鏈接。現(xiàn)在,讓我們總結(jié)一下我們所涵蓋的內(nèi)容。
回顧
在本教程中,您了解了git rebase,一個(gè)重寫(xiě) Git 歷史記錄的超級(jí)強(qiáng)大工具。您考慮了一些git rebase有用的用例,以及如何使用一個(gè)、兩個(gè)或三個(gè)參數(shù)(帶或不帶開(kāi)關(guān))來(lái)使用它--onto。
我希望我能夠讓您相信這git rebase很強(qiáng)大,而且一旦您掌握了要點(diǎn),它就非常簡(jiǎn)單。它是一個(gè)“復(fù)制粘貼”提交(或更準(zhǔn)確地說(shuō),補(bǔ)?。┑墓ぞ?。它是一個(gè)值得擁有的有用工具。