[英]Git rebase conflict, which branches are actually modified?
所以即使做了很多時間,我仍然非常害怕變基,我認為我遇到的一個問題是我確實深刻理解它的作用。
所以我有分支開發和我的分支,從開發開始。 為了避免/解決沖突,我希望更新我的分支從哪個提交開始。 出於這個原因,在我的分支上,我執行了git rebase develop
。
我的問題是,假設在變基階段,我決定刪除/修改執行的每一個更改。 一旦我推送,是否只會修改我的分支,或者我的 rebase 是否也修改了來自開發的實際提交?
只有您當前所在的分支會被修改。 您正在變基的分支不會被觸及:它僅用作工作的起點。
如果你想更深入地了解 git,我推薦閱讀A Hacker's Guide to Git ,這是一篇深入淺出的好文章。 它確實提高了我對 git 的工作原理及其作用的理解。 下面顯示的是文章的摘錄:
正如kapsiR 所說,不要害怕。 好吧,也許有一點點,足以采取一些預防措施,比如經常運行git status
。
git rebase
的真正目的是將(一些)提交復制到新的和改進的提交。 要了解其工作原理和原因,您首先需要了解提交:
這是前幾個關鍵問題。 當您知道答案時,您將了解 Git 本身。 然后git rebase
只需要一項:
(雖然總會有更多的東西要學)。
Git 中的提交:
有編號。 每個提交都有一個唯一的hash ID 。 這是一個非常大的、無法記住的數字,以十六進制表示。 每當您進行新的提交時,該提交都會獲得一個唯一編號。 我所說的獨特並不是指“可能獨特”或“某種獨特”,而是絕對獨特。 現在,宇宙中任何地方都不允許其他 Git 擁有或使用該號碼,除非您向其他 Git 提供您剛剛提交的此提交。 然后他們將使用此編號進行此提交,並且兩個存儲庫將具有相同的提交。
這意味着提交的編號——它的 hash ID——在某種意義上是提交。 當您將兩個 Git 版本相互連接時,將提交從一個存儲庫交換到另一個存儲庫時,他們只需查看數字。 如果他們有相同的號碼,他們有相同的東西。 如果沒有,誰丟了號碼,可以從另一個Git那里得到東西,現在他們有了同樣的東西,同樣的號碼。
存儲兩個東西:
每個提交都存儲每個文件的完整快照。 提交中的文件以特殊的、壓縮和去重的、僅 Git 的形式存儲:只有 Git 可以實際讀取這些內容,沒有任何東西——甚至 Git 本身——可以覆蓋它們。 它們無法更改的事實允許重復數據刪除,並防止存儲庫快速變得非常胖,即使大多數提交主要重用了以前提交的大部分文件。
每個提交還存儲一些元數據,或有關提交的信息。 例如,這包括提交人的姓名和 email 地址。 它包括一些日期和時間戳。 它包含您的日志消息,因此您可以稍后回顧並記住您提交的原因,或者閱讀其他人解釋他們提交的原因的文本。 (好的提交信息很重要。 )
Git 將部分元數據用於自己的目的,這在這里至關重要。 當您進行新的提交時,Git 會在該提交的元數據中存儲一些先前提交的 hash ID。 大多數提交都得到一個存儲在這里的 hash ID; 我們稍后會看到更多關於這個的信息。
是只讀的:任何Git 提交的任何部分都不能更改。
事實上,Git 的神奇 hash 技術意味着任何類型的內部 object 都無法改變。 這提供了提交的只讀性質、去重文件的只讀性質以及去重文件的能力。
為什么我們關心這個很簡單。 Git 都是關於提交的。 Git 與文件或分支(即分支名稱)無關。 這是關於提交的。 提交是 Git 存儲庫之間的交換貨幣。 我們將一個 Git 連接到另一個,使用git fetch
或git push
, 1我們將提交從一個 Z0365Z1 轉移到另一個 Z0365Z105ADFE279
Git 存儲庫首先是一個大型的提交數據庫。 提交保存文件和元數據。 我們自己的個人目標很可能與文件有關,但 Git 在這個級別不處理文件。 它只處理提交。 所以我們必須使用提交來處理文件。
1 git pull
命令是一種方便的包裝器,它首先運行git fetch
,從某個地方獲取新的提交,然后運行第二個 Git 命令來執行一些操作。 這有點陷阱:它使人們相信git pull
是正確的命令,而實際上這兩個單獨的命令通常是正確的,特別是因為您可以在它們之間擠壓一個命令。 也就是說,您可以在決定如何甚至是否要合並它們之前獲得新的提交並查看它們。 當您使用git pull
時,您不會得到這個選擇。
我在上面提到過,每個提交都存儲了之前提交的 hash ID。 或者,更准確地說,每個提交都有一個先前提交的列表:列表可以是空的(“我沒有父母,我是孤兒”),或者只有一個條目(“我的爸爸/媽媽是 ________”——填寫具有 hash ID 的空白),或兩個或多個長條目(“我是一個合並。我的父母是 ________ 和 ________”——再次填寫空白)。
請注意,沒有父母知道它的孩子。 當提交“出生”時,它知道它的父母是誰,但從那時起它就一直被凍結。 它無法學習其子代的“名稱”(哈希 ID)。 因此,存儲庫中的歷史記錄是反向的。
我們從稍后的提交開始找到提交。 后面的每一個都向后指向其父級。 只要我們沒有合並提交,我們就有一個簡單的線性鏈,如下所示:
... <-F <-G <-H
這里H
代表鏈中最后一個提交的真實(又大又丑)hash ID。 我們必須以某種方式知道它的 hash ID - 我們稍后會回到這個問題 - 但假設我們確實知道它的 hash ID,這足以讓 Z0BCC70105AD279503E51FE7B3F47B6 的所有提交從其大數據庫中檢索提交。
檢索到H
后,Git 現在有了一個快照——所有 go 和H
的文件——以及一些元數據。 元數據包括早期提交G
的 hash ID。 所以 Git 現在可以回到它的數據庫並拉出提交G
,現在它有另一個快照和更多元數據。
通過比較兩個快照中的文件G
和H
, Git 可以告訴我們哪些文件發生了變化,哪些沒有。 (這也很快,因為重復數據刪除。未更改的文件是共享的,即兩個提交引用相同的底層文件。)Git 可以更仔細地查看確實更改的文件,並且向我們展示這些文件中發生了什么變化。 這就是我們通常如何看待提交:作為自上次提交以來的更改。 但是 Git 不存儲更改; 它將整個文件存儲為快照。
向我們展示了H
的元數據和更改后,Git 現在可以后退一跳以提交G
(它已經檢索到其 hash ID)。 這當然是一個提交,帶有快照和元數據。 它的元數據指的是較早的提交F
。 所以 Git 現在可以重復它剛剛對H
所做的事情,向我們展示提交G
。
顯示提交G
后,Git 現在可以后退一跳提交F
它可以向我們顯示F
,然后再次向后移動一跳。 這一直持續到我們到達第一個提交。 第一次提交在一個方面很特別:它沒有父級。 它的“先前提交”列表是空的。 這就是 Git 知道何時停止倒退的方式。 (在大型存儲庫中,您可能會在 Git 回到開始之前很久就退出git log
,但這也很好。)
不過這里有一個大問題。 我們如何找到提交H
? 我們在上面說過,我們只是假設我們在某處保存了 hash ID。 也許我們把它寫在一張紙上,或者寫在辦公室的白板上,或者其他什么地方。 但這里有一個更好的主意:我們有一台計算機,運行帶有提交數據庫的軟件。 讓我們也有一個最新的 hash ID的數據庫。 我們可以稱這些分支名稱。
像master
或main
、 develop
、 feature
等分支名稱只包含一個提交 hash ID 。 存儲在分支名稱中的一個 hash ID是鏈中最后一個提交的 hash ID。 所以如果我們有:
...--F--G--H <-- main
那么根據定義,提交H
是分支main
上的最新提交。
我們可以制作更多的分支名稱。 讓我們添加名稱develop
,也指向提交H
:
...--F--G--H <-- develop, main
現在,我們需要某種方法來知道我們正在使用哪個名稱——盡管無論我們使用哪個名稱,我們都將使用提交H
所以讓我們添加特殊的全大寫名稱HEAD
:
...--F--G--H <-- develop, main (HEAD)
我們目前on branch main
,因為HEAD
附加到(或旁邊) main
。 如果我們git checkout develop
或git switch develop
,我們得到:
...--F--G--H <-- develop (HEAD), main
我們仍在使用提交H
,所以沒有其他任何改變,但我們通過名稱develop
使用它。
當我們進行新的提交時——我將在這里完全跳過 Git索引(即暫存區)的大部分細節——我們運行git commit
和 Git:
HEAD
查找當前提交的 hash ID H
;HEAD
所附加的名稱中。 所以現在我們已經做了一個新的提交I
,新的提交I
指向現有的提交H
。 並且因為HEAD
與develop
相關聯,因此名稱develop
現在指向新的提交I
名稱main
仍然指向提交H
:
...--F--G--H <-- main
\
I <-- develop (HEAD)
沒有其他改變:提交H
沒有改變(它沒有向前指向I
; I
向后指向H
)。 HEAD
仍然依附於develop
。 唯一的變化是我們有一個新的提交I
,帶有新的元數據和快照,並且新提交I
的 hash ID 現在存儲在develop
中。
假設您現在創建自己的新分支名稱fix-123
,並切換到該分支:
...--F--G--H <-- main
\
I <-- develop, fix-123 (HEAD)
現在你做了兩個新的提交J
和K
:
...--F--G--H <-- main
\
I <-- develop
\
J--K <-- fix-123 (HEAD)
現在假設其他人對develop
做了一個新的提交。 你git checkout develop
或git switch develop
得到:
...--F--G--H <-- main
\
I <-- develop (HEAD)
\
J--K <-- fix-123
然后你獲得他們的新提交( git fetch
+ git merge
,也許,或者git pull
如果你使用速記的一體式命令,那么它適用於實例,現在和干燥器組合)你有:
...--F--G--H <-- main
\
I--L <-- develop (HEAD)
\
J--K <-- fix-123
現在是 rebase 的時候了。
我們現在將停止在main
中繪制,只需使用:
...--I--L <-- develop (HEAD)
\
J--K <-- fix-123
使事情更緊湊。 我們現在的問題是提交JK
。 它們並沒有什么問題,除了......好吧,問題是提交J
作為其父級,提交I
。 我們想要一個以提交L
作為其父級的提交。
任何現有的提交都不會改變。 我們無法修復提交J
。 但是我們可以做出一個非常像J
的新提交,只是不同而已。 我們也可以對提交K
做同樣的事情。 我們想要得到的是這樣的:
J'-K' <-- new-and-improved-fix-123 (HEAD)
/
...--I--L <-- develop
\
J--K <-- old-and-lousy-fix-123
這里J'
是我們的J
副本,而K'
是我們的K
副本。 J
vs J'
有兩點不同。 K
vs K'
也有兩點不同。 特別是,兩個副本都有不同的父提交,我們可以從圖中看到。 兩個副本的快照也有任何差異,基於L
中的快照而不是I
中的快照。
為了到達這里,從我們所在的地方,我們需要:
J
和K
;L
處創建一個新的分支名稱;J
; 和K
。當我們完成所有這些后,我們有了新的圖表,我們現在要做的就是修復分支名稱。
有一種手動執行此操作的方法,甚至不是那么難:
git checkout -b new-fix-123 develop
git cherry-pick <hash-of-J>
git cherry-pick <hash-of-K>
我們可以讓它更短:
git checkout -b new-fix-123 develop
git cherry-pick <hash-of-J> <hash-of-K>
將其簡化為兩個命令。 但是我們仍然需要將fix-123
名稱移動到我們現在所在的位置,並再次檢查fix-123
:
git checkout -B fix-123
會這樣做,然后我們可以刪除new-fix-123
,我們將擁有:
J'-K' <-- fix-123 (HEAD)
/
...--I--L <-- develop
\
J--K <-- ???
請注意,不再有任何名稱可以用來查找提交K
。 我們強制 Git 將名稱fix-123
移動到指向K'
。 舊的提交仍然存在。 我們再也找不到他們了。
git rebase
命令一步完成同樣,我們從以下開始:
...--I--L <-- develop (HEAD)
\
J--K <-- fix-123
跑步:
git checkout fix-123
git rebase develop
有 Git:
列出“on” fix-123
但不是“on” develop
的提交:那是 hash IDs J
和K
。 Git 實際上是向后生成此列表 - 因為 Git 總是向后工作 - 但git rebase
然后反轉向后列表,使其向前。
在提交L
處創建一個臨時“分支”。 Git 為此使用分離的 HEAD模式,而不是使用分支名稱。 我們將在此處跳過詳細信息,但如果出現問題,它們很重要。
為列表中的每個提交運行git cherry-pick
。
將我們所在的分支名稱 — fix-123
— 強制到此處並再次檢查。
這正是我們手動執行的操作,但 Git 會自動完成所有操作。 只要沒有出錯,我們最終就會得到我們想要的。 這里真正的訣竅是從失敗中恢復。
“復制”一個提交——使用cherry-pick,或者使用git rebase
在舊版本 Git 中使用的更原始的方法——可能會失敗。 特別是每個git cherry-pick
操作:
從技術上講,Git 所做的就是使用它的合並引擎。 這通常工作得很好,但它可能會因合並沖突而停止。 當它發生時,您必須解決沖突,然后繼續變基。
當 Git 去確定要復制的提交時,有時您沒有得到預期的提交集。 您可以在這里使用git rebase --onto
來提供幫助,但我不會在這個答案中多說什么。 如果您有一組包含任何合並提交的提交,它們會使事情復雜化。 我根本不打算在這里介紹它們。 Git 現在(從 2.22 開始)有一個--rebase-merges
選項可以完成這項工作,但這有點棘手。
最后,如果一個 rebase 錯誤go並且你希望你沒有首先啟動它......好吧,如果你被困在失敗的 rebase 中間,你可以使用git rebase --abort
:
J' <-- HEAD
/
...--I--L <-- develop
\
J--K <-- fix-123
當復制K
失敗時,可能是因為與L
發生沖突,並且您決定更願意回到這個:
J' [abandoned]
/
...--I--L <-- develop
\
J--K <-- fix-123 (HEAD)
一個簡單的git rebase --abort
就足夠了。 但是,如果您已經讓 rebase 完成,或者解決了沖突並繼續 rebase 並且現在處於:
J'-K' <-- fix-123 (HEAD)
/
...--I--L <-- develop
\
J--K [abandoned]
並確定整個事情是一個錯誤,您需要找到原始提交K
的 hash ID才能恢復。
有一種方法可以做到這一點,使用git reflog
。 但這是一種痛苦。 幸運的是,有一個更簡單的方法。 如果您即將開始一個 rebase,並且不確定要使用結果還是堅持原來的,只需在開始之前創建一個新分支:
...--I--L <-- develop
\
J--K <-- fix-123.0, fix-123 (HEAD)
現在,在你的 rebase 之后,你將擁有:
J'-K' <-- fix-123 (HEAD)
/
...--I--L <-- develop
\
J--K <-- fix-123.0
以K
結尾的舊系列提交仍然很容易找到,使用分支名稱fix-123.0
。 2如果我發現自己再次變基,我會在開始之前制作一個新的fix-123.1
。 所以fix-123
是當前的,編號的是以前的,如果我想要它們,一旦我確定我完成了它們,就可以刪除它們。
2對於像我這樣的計算機老手,您通常可以通過我們是否從零開始數數來判斷我們是從數學系還是物理/工程系出來的。 我兩者都做過,但我的心更多地與數學家在一起。 有時我想我應該把這些名字寫得更詳細一些,例如,上面有日期,但簡單的編號似乎效果很好。 我很少會高於 3 或 4。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.