![](/img/trans.png)
[英]Merged and pushed feature branch to the wrong remote branch. How to fix?
[英]Merged Master into branch then committed and pushed changes to branch. How can this be undone without a force push?
我犯了一個錯誤。
我有一個分支( A
),它是從Master
分支出來的。 Master
領先A
不少。 前幾天不小心把Master
合並到A
里面推了。 那天晚些時候我注意到了我的錯誤,但不知道如何修復它,所以我嘗試添加一些功能標志來關閉不應在A
中啟用的功能並推送它們。 后來,我決定嘗試恢復A
分支以擺脫所有Master
提交。 我完成了所有更改(大約 100 個文件),現在A
看起來就像在Master
之前所做的那樣。 但是,我現在的問題是,如果沒有合並嘗試刪除Master
中存在的所有更改,我就無法將A
合並到Master
中。 (即,在Master
中創建的新文件在A
的恢復中被刪除,所以現在 git 想要從Master
中刪除文件,如果我嘗試將A
合並到Master
。)
我怎樣才能修復我的巨大錯誤,然后回到我可以在A
上進行維護補丁並相應地與Master
合並的地方,以便將來的版本不會丟失補丁?
我 C。 你大發雷霆。 最好的方法是刪除 master 到 A.. 的合並,但它的價格很高。 不太優雅的方法是取消恢復從 A 恢復的與 master 相關的更改,以便它們不再消失。 然后,當您將 A 合並到 master 時,您將不會看到那么大的逆轉。 我不喜歡它,但是......
如何在沒有強制推送的情況下撤消合並的簡短答案? 是:你不能。
更長的答案是你不能,但你也不需要,只要你知道你在做什么以及合並是如何工作的; 如果您可以說服所有其他用戶以這種方式強制使用任何存儲庫,那么強制推送有時會更方便。
請參閱如何恢復已推送到遠程分支的合並提交? 另請參閱MK446 對同一問題的回答,這幾乎是 Linus Torvald 關於恢復合並恢復的描述的復制粘貼。
理解為什么會出現這種情況以及如何處理的關鍵是要認識到任何一組提交的“合並性”是提交本身所固有的。 分支名稱僅用作查找提交的方法。 強制推送的行為是一種更改名稱指向的方法,以便人們(和 Gits)無法再找到某些提交。
拿到手之后就很容易看出來,但是我還是不知道怎么解釋,只能說服人畫圖。 Linus Torvalds 以這種方式總結了它——這是准確的,但很棘手:
[雖然] 恢復合並提交...撤消提交更改的數據,...它對合並對歷史的影響絕對沒有任何作用。 所以合並仍然存在,它仍將被視為將兩個分支連接在一起,未來的合並將把合並視為最后一個共享的 state - 恢復引入的合並的恢復根本不會影響它。 因此,“還原”撤消了數據更改,但從某種意義上說,它不是“撤消”,因為它不會撤消提交對存儲庫歷史的影響。 因此,如果您將“還原”視為“撤消”,那么您將永遠錯過這部分還原。 是的,它會撤消數據,但不,它不會撤消歷史記錄。
“歷史”是提交圖。 該圖由提交確定,但我們通過分支名稱找到提交。 因此,我們可以通過更改存儲在名稱中的 hash ID 來更改我們可以看到的內容。 但是,除非您知道並在自己的腦海中看到它是如何工作的,否則這並沒有真正的幫助。
您可能會花一些時間查看Think Like (a) Git上的教程,但為了快速查看,請考慮以下事實:
一個 Git 提交由兩部分組成:它的主要數據,它是你所有文件的快照——我們將在這里多說一點——以及它的元數據,它包含關於提交本身的信息。 大多數元數據都是供您稍后自己了解的信息:誰進行了提交,何時以及他們的日志消息告訴您他們為什么進行該提交。 但是元數據中的一項是針對 Git 本身的,即父提交 hash ID的列表。
存儲在任何 Git 提交中的所有內容——實際上,在任何 Git object中,但大多數情況下你直接處理提交對象——都是完全只讀的。 原因是 Git 通過 hash ID找到object。 Git 有一個存儲這些對象的大鍵值數據庫; 鍵是 hash ID,值是對象的內容。 每個key唯一標識一個object,並且每個commit都是不同的, 1所以每個commit都有一個唯一的hash ID。 2
因此,提交的 hash ID 實際上是該提交的“真實名稱”。 每當我們將 hash ID 存儲在某處時,例如,在文件中,或電子表格中的一行,或其他任何地方,我們說這個條目指向提交。
因此,存儲在每個提交中的父 hash ID指向先前的提交。 大多數提交只有一個父 hash ID; 使提交成為合並提交的原因是它具有兩個或多個父 hash ID。 Git 確保每當有人進行新提交時,該提交中列出的父 hash ID 是現有提交的 ID。 3
所有這一切的結果是,大多數普通提交以簡單的線性方式向后指向。 如果我們繪制一系列提交,用單個大寫字母替換真實的 hash ID,將較新的提交放在右側,我們得到:
... <-F <-G <-H
其中H
代表鏈中最后一次提交的 hash ID。 提交H
指向(包含原始 hash ID)其父提交G
; 提交G
指向更早的提交F
; 等等。
因為 hash ID 看起來非常隨機, 4我們需要一些方法來找到鏈中的最后一個提交。 另一種方法是查看存儲庫中的每個提交,構建所有鏈,並使用它來確定哪些提交是“最后的”。 5這太慢了:所以 Git 給了我們分支名稱。 像master
或dev
這樣的分支名稱只是指向一個提交。 無論名稱指向什么提交,我們都認定這是分支的尖端提交。 所以給出:
...--F--G--H <-- master
我們說 commit H
是分支master
的提示提交。 6我們說所有這些提交都包含在分支master
中。
多個名稱可以指向任何一個特定的提交。 如果我們有:
...--G--H <-- dev, master
然后兩個名稱dev
和master
將提交H
標識為其分支提示提交。 通過並包括H
的提交在兩個分支上。 我們將git checkout
出這些名稱之一以開始使用提交H
; 如果我們隨后添加一個新提交,則新提交將以提交H
作為其父級。 例如,如果我們在“on”分支master
時添加一個新的提交,新的提交將是提交I
,我們可以這樣繪制:
I <-- master (HEAD)
/
...--G--H <-- dev
特殊名稱HEAD
可以附加到一個分支名稱上——一次只能附加一個; 它指示哪個分支名稱新提交更新,並向我們顯示哪個提交是我們當前的提交,哪個分支名稱是我們的當前分支。
添加另一個提交到master
,然后簽出dev
,得到我們這個:
I--J <-- master
/
...--G--H <-- dev (HEAD)
當前提交現在倒回到H
,當前分支是dev
。
1這就是提交具有日期和時間戳的原因之一。 即使兩個提交在其他方面相同,但如果它們是在不同時間進行的,它們具有不同的時間戳,因此是不同的提交。 如果您在完全相同的時間進行兩次完全相同的提交,那么您只進行了一次提交......但是如果您在完全相同的時間多次執行完全相同的事情,您實際上做了很多事情,還是只做一件事?
2根據鴿巢原理,如果“所有提交”的空間大於“提交 hash IDs”的空間——並且確實如此——必須有多個不同的提交解析到相同的 hash ID。 Git 對此的回答部分是“你不能使用那些其他提交”,但也是“那又怎樣,它在實踐中從未發生過”。 另請參閱新發現的 SHA-1 沖突如何影響 Git?
3不這樣做可能會導致 Git 存儲庫損壞,“連接性”不正確。 每當您看到有關“檢查連接性”的 Git 消息時,Git 就會進行此類檢查。 一些新的 Git 工作故意削弱這些連接檢查,但即使 Git 有時不檢查,至少原則上仍然存在規則。
4當然,它們完全是確定性的——它們目前是 SHA-1 哈希——但它們具有足夠的不可預測性,看起來是隨機的。
5 git fsck
和git gc
都這樣做,以便確定是否有一些提交可以被丟棄。 git fsck
命令會告訴你它們——它們是懸空的和/或無法訪問的提交。 如果其他條件合適, git gc
命令將刪除它們。 特別是,它們需要老化超過過期時間。 這避免了git gc
刪除仍在構建的提交。 提交和其他對象可能無法訪問,因為創建它們的 Git 命令尚未完成。
6這給我們留下了各種各樣的難題:Git 中的分支一詞是模棱兩可的。 它是表示分支名稱,還是表示提示提交,還是表示某些以指定提交結尾的提交? 如果是后者,規范是否必須是分支名稱? 這個問題的答案通常是肯定的:分支這個詞可以表示所有這些,甚至更多。 另請參閱“分支”到底是什么意思? 因此,最好盡可能使用更具體的術語。
現在我們在dev
和 commit H
上,我們可以再添加兩個提交來生成:
I--J <-- master
/
...--G--H
\
K--L <-- dev (HEAD)
此時,我們可以git checkout master
然后git merge dev
。 如果提交是 Git 存在的理由,那么 Git 的自動合並是我們都使用Git 而不是其他一些 VCS 的重要原因。 7 git merge
所做的是執行三向合並,將合並基礎快照與兩個提示提交快照相結合。
合並基礎完全由提交圖決定。 在這個特定的圖表中很容易看到,因為合並基礎是兩個分支上的最佳提交。 8所以git merge
會做的是:
H
中的快照與我們當前分支提示提交中的快照進行比較,以查看我們更改了什么; 和H
中的快照與其分支提示提交中的快照進行比較,以查看它們更改了什么, 然后簡單地(或復雜地,如果需要)組合這兩組更改。 現在可以將組合更改應用於基本快照,即在提交H
中始終保存的文件。
組合這兩個變更集的結果要么是成功——一個新的快照准備好 go 進入一個新的提交——要么是一個合並沖突。 每當 Git 無法自行組合我們的更改及其更改時,就會發生沖突情況。 If that happens, Git stops in the middle of the merge, leaving a mess behind, and our job becomes clean up the mess and provide the correct final snapshot and then tell Git to continue: git merge --continue
or git commit
(both do同樣的事情)。
成功組合更改后——也許在我們的幫助下——Git 現在進行了新的提交。 這個新提交就像任何其他提交一樣,因為它有一個數據快照,並且有一些元數據給出我們的名字和 email 地址、當前日期和時間等等。 但它的特殊之處僅在於一種方式:作為其父母(復數),它具有兩個提示提交的 hash ID。
與任何提交一樣,提交的行為會更新當前分支名稱,因此我們可以繪制如下結果:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- dev
請記住,我們從git checkout master
開始該過程,因此當前提交是J
並且當前分支名稱是,並且仍然是master
。 當前提交現在是合並提交M
,它的兩個父級依次是J
(如果您願意,可以稍后使用J
的第一個父級)和L
。
7許多前 Git VCS 具有內置的合並功能,但沒有那么多具有如此聰明和自動的合並功能。 過去和現在都有其他很好的版本控制系統,但Git也增加了分布式版本控制,並與GitHub等站點一起贏得了網絡效應。 所以現在我們被 Git 困住了。 Mercurial 在用戶友好性方面明顯優於 Git,而且 Bitbucket 曾經是 Mercurial 專用站點,但現在......不是了。
8在這里,我們使用分支一詞來表示可從當前分支提示到達的提交集。 我們知道分支名稱稍后會移動:在將來的某個時候, master
不會命名 commit J
和/或dev
不會命名 commit L
,但現在他們會這樣做。 所以我們發現提交可以從J
到達並向后工作,並且提交可從L
到達並向后工作,當我們這樣做時,兩個分支上明顯的最佳提交是提交H
git merge
並不總是合並在一種特定(但常見)的情況下, git merge
不會進行合並提交,除非您強制它這樣做。 特別是,假設兩個分支上的最佳共享提交是“后面”分支上的最后一個提交。 也就是說,假設我們有:
...--o--B <-- br1 (HEAD)
\
C--D <-- br2
其中D
的父級是C
, C 的父C
是B
,依此類推。 我們已經檢查了br1
,如HEAD
所示。 如果我們運行git merge br2
B
Git 將像往常一樣找到提交B
和D
,從D
向后工作到C
,發現最好的提交也是在當前共享分支上提交B
犯罪。
如果我們此時進行真正的合並,Git 將比較B
中的快照與B
中的快照:base vs HEAD
是B
vs B
。 顯然這里沒有變化。 然后 Git 將比較B
中的快照與D
中的快照。 無論這些更改是什么,Git 都會將這些更改應用於B
中的快照。 結果是...... D
中的快照。
因此,如果 Git 在這一點上進行真正的合並,它將產生:
...--o--B------M <-- br1 (HEAD)
\ /
C--D <-- br2
其中M
中的快照與D
中的快照完全匹配。
您可以強制 Git使用git merge --no-ff
進行真正的合並,但默認情況下,Git 將“作弊”。 它會對自己說:合並快照將匹配D
,因此我們可以直接將名稱br1
指向提交D
。 所以git merge
將簡單地git checkout D
,但也滑動名稱br1
"forward" 指向提交D
:
...--o--B
\
C--D <-- br1 (HEAD), br2
如果您使用 GitHub 進行合並,請注意GitHub 始終強制進行真正的合並,因此您永遠不會快進。 9
9最接近的方法是使用 GitHub 的變基和合並模式,但這會復制原本可以快速向前合並的提交。 它為他們提供了新的提交者名稱和電子郵件以及時間戳,並且生成的提交具有新的 hash ID。 所以它從來都不是真正的快進。 這有時很煩人,我希望他們有一個真正的快進選項。
假設我們已經完成了這個模式一段時間,並且有:
...--o--o--o------A-----M <-- master
\ / /
o--o--o--o--B--C--D <-- dev
哪個提交是master
和dev
的合並基礎? 這里有一個很大的提示:它是一個有字母的提交,而不是更無聊的歷史o
提交。
棘手的部分是要找到合並基礎,當我們從分支提示提交向后走時,我們應該同時訪問兩個父節點。 所以合並提交M
有兩個父母, A
和B
。 同時,從D
開始並向后工作,我們也到達了提交B
(經過兩跳)。 所以提交B
是合並基礎。
B
是合並基礎的原因是合並提交M
的存在。 名稱master
指向M
和M
指向兩個提交, A
和B
。 提交B
是“on”(包含在)分支master
,並且顯然是on/contained-in branch dev
,只要master
指向提交M
或到達(通過某些提交鏈)合並M
。
Git 通常只會向分支添加提交,有時通過提交一次一個,有時通過合並或快進一次添加多個。 一旦提交B
通過提交M
變為“on”(包含在)分支master
,它將繼續是 on/contained-in master
。 未來的合並可能會找到比提交B
更好的提交,但只要這些提交繼續在master
和dev
上,提交B
將始終是基於合並的候選者。
所以這就是為什么你不能在沒有強制推送的情況下“撤消合並”。 您可以更改新提交中的快照(例如,這就是git revert
的內容),但您無法更改現有提交的歷史記錄。 歷史是通過遍歷圖找到的一組提交,所有現有的提交都將一直凍結,只要可以找到它們就會保留在圖中:
...--o--o--o------A-----M <-- master
\ / /
o--o--o--o--B--C--D <-- dev
master
的歷史是提交M
,然后都提交A
和B
,然后是他們的父母等等。 dev
的歷史是提交D
,然后提交C
,然后提交B
,依此類推。
要更改從master
看到的歷史記錄,您必須說服 Git 停止遍歷提交M
如果您使用 force-push 從master
中刪除M
——它仍然存在,只是不再通過master
找到它——你會得到:
------M ???
/ /
...--o--o--o------A <-- master
\ / /
o--o--o--o--B--C--D <-- dev
(請注意,在此圖中沒有找到M
的名稱,因此最終git gc
將完全丟棄提交M
另請參見腳注 5。)
Force-push 是我們告訴 Git 的方式:是的,此操作將導致某些提交無法訪問,並且可能永遠丟失它們。 我們的意思是讓這發生! 通過完全刪除合並提交M
,我們返回到 state ,其中從未發生合並,並且下次提交B
不會成為合並基礎。
(練習:找到合並基。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.