![](/img/trans.png)
[英]git: I accidentally merged feature branch into master instead of develop
[英]I merged branches develop into master but on master branch no changes from develop
除非我們有權訪問您的特定存儲庫,否則我們無法對您的特定存儲庫進行任何具體說明。 不過,總的來說,這種說法:
分支開發沒有變化
是沒有意義的。 原因是 Git 分支沒有變化。 事實上,Git 甚至不存儲更改。 相反,Git 存儲提交,並且提交保存快照,而不是更改。 要了解git merge
的作用,您必須首先深入了解提交:提交是什么,對您有什么作用,以及您如何獲得對提交的訪問權限。
任何 Git 存儲庫的核心是一個包含提交的大型數據庫(以及支持所有這些所需的其他內部 Git 對象)。 These commits (and other objects) are numbered, but the numbers aren't simple counting numbers: they don't go commit #1, #2, #3, etc. Instead, each commit or other internal Git object has a hash ID :一大串難看的字母和數字,以十六進制表示一個非常大的數字。 1這些 hash ID 看起來是隨機的,但實際上並非如此:它們實際上是 object內容的加密校驗和(提交的內容,用於提交對象)。
這些數字——hash ID——是 Git 實際找到提交的方式。 hash ID 是鍵值數據庫中的鍵; 存儲的數據,必須 hash 回到用於查找它的密鑰,即存儲的 object。 這意味着一旦提交,或任何其他內部 object,就永遠無法更改。 如果您從數據庫中取出存儲的 object,對其進行更改,然后將其存儲回來,它會使用一個新的不同的鍵,而舊的 object 將保持在數據庫中,保持不變。 因此,從字面上看,提交是無法更改的。
每個提交包含兩個有用的數據塊。 其中之一,我們稱之為metadata ,因為它是關於提交本身的數據。 這是Git保存提交的人的名稱和 email 地址等內容的地方。 提交的另一個重要部分是主要數據,這是每個文件的保存快照,與您或任何人運行git commit
時文件的形式相同。
請注意,這意味着提交中沒有更改。 每個提交的數據都是每個文件的完整副本。 每個文件的完整副本是只讀的(因為任何提交的任何部分都不能更改)。 為了節省空間,提交中的文件以一種特殊的僅 Git 格式存儲——根本不像普通的計算機文件——這是經過壓縮和重復數據刪除的. 因此,每個提交都可以共享來自先前提交的單個文件數據。 由於這些內容都是只讀的,因此可以安全地共享:沒有人,甚至 Git 本身,都不能更改保存的文件內容數據(它使用與 hash ID 相同的鍵值數據庫作為提交)。
還有另一件重要的事情要知道。 每個提交的元數據包含一個先前提交 hash ID的列表,Git 為該提交存儲了這些 ID。 大多數提交只有一個先前的提交。 2這對您來說意味着提交形成簡單的后向鏈:
... <-F <-G <-H
在這里, H
代表鏈中最后一個提交的實際大丑 hash ID。 只要我們知道這個提交的 hash ID——例如,也許我們把它寫在紙上,然后每次我們想要它時再次輸入——我們就可以讓 Git提取這個提交,包括它的數據(快照)和元數據(信息關於提交)。 在元數據中,我們(或 Git)可以找到先前提交G
的實際 hash ID。
當然,較早的提交G
既有數據又有元數據。 G
的元數據包含更早提交F
的實際 hash ID,因此 Git 可以從G
中找到F
F
反過來又指向另一個更早的提交,這一直持續到有史以來的第一個提交,然后停止。
每個提交中的文件都被永久存儲(或者只要提交本身持續存在)。 我們可以讓 Git 隨時將它們從提交中復制到普通文件中。 它們以只有 Git 可以使用的形式存儲,沒有其他東西可以更改,但是一旦我們提取它們,我們就可以使用和更改它們。 這個提取過程稱為檢查提交。 3
1目前,可能數字的范圍從 1 到 2 160 -1 或 1461501637330902918203684832716283019655932542975。 這比 10 ^ 48 多一點。 未來范圍會更大,達到 2 256 -1——大約 10 77個可能的數字。
2通常的例外是第一次提交,它不能有任何先前的提交,因此沒有,以及合並提交,它將兩個先前的提交聯系在一起,因此記住每個人的 hash ID。 具有一個先前提交 ID 的提交是普通提交。 沒有提交的提交是根提交——從第一次提交開始,通常只有一個提交——根據定義,有兩個或更多的提交是合並提交。
3我們實際上在現代 Git 中獲得了幾種不同的提取方式。 在舊版本的 Git 中,存在從舊提交中提取特定文件的問題,但現在git restore
存在,我們可以正確完成。 不過,這個答案不會涉及到 Git 的index的更詳細的細節。
git log
和屏幕截圖如何工作這個向后看的鏈是允許git log
工作的原因。 給定某個起點,或者如果您願意,可以使用多個起點,Git 可以找到這些起點提交。 這些提交指向更早的提交,因此 Git 可以使用這些來查找更早的提交。
不過,要開始,Git 需要一些最后一次提交的 hash ID。 這些將來自哪里? 上面,我建議我們可以在紙上寫下提交H
的實際 hash ID。 但如果我們這樣做,然后重新輸入,我們就會出錯。 我們不必這樣做:我們有一台計算機,計算機可以為我們記住這些 hash ID。
這就是名稱——分支名稱、標簽名稱和 Git 的所有其他名稱——的用途。 每個都進入第二個數據庫 - 另一個鍵值存儲- 按名稱而不是 hash ID 進行索引。 第二個數據庫中的值是 hash ID。 然而,這個數據庫是可寫的:我們可以隨時為每個分支名稱替換hash ID。 object 數據庫只允許添加新對象,但名稱數據庫允許我們隨時覆蓋任何名稱。
我們需要謹慎使用這種能力,因為根據定義,無論 hash ID 存儲在分支名稱中,都是我們要稱為“分支的一部分”的最后提交。 在 Git 術語中,分支名稱包含提示提交的 hash ID。 鏈的末端是該分支中的最后一次提交,即使鏈繼續運行:
...--G--H <-- main
\
I--J <-- feature
在這里,我們有兩個分支名稱, main
和feature
。 名稱feature
定位提交J
(通過存儲其 hash ID)。 提交J
指向提交I
,后者指向提交H
。 名稱main
位於提交H
。 提交H
點返回提交G
,依此類推。
這意味着通過並包含H
的提交都在兩個分支上。 Git 在這里有點奇怪:許多版本控制系統不會做這樣的事情,即一次提交多個分支。 但是 Git 可以,所以如果你要使用 Git,你必須接受它。
假設我們有這一系列快照:
...--F--G--H <-- master
最后一個快照,即分支master
的尖端,是提交H
中的快照。 但是還有更多的快照。 例如,提交G
中有一個。
如果我們要求 Git 將提交G
(即它的快照)提取到一個位置,並將H
提交到另一個位置,會發生什么? 我們可以比較所有這些文件。 其中一些可能完全相同。 在這種情況下,Git 將對文件內容進行重復數據刪除,因此G
和H
實際上只是共享這些文件。 其他的會有所不同: G
和H
擁有這些文件的不同副本。
相同的文件不是很有趣。 讓我們把它們扔掉(也許通過從兩個位置的簽出副本中刪除它們)。 剩下的仍然是快照,但我們現在可以在Spot the Difference游戲中並排比較它們。 無論這些文件發生了什么變化,嗯,這就是提交G
和提交H
之間的變化。
如果我們要求 Git 或其他查看軟件將提交顯示為更改,這就是它的作用。 它“提取”(在內存中)兩個提交,使用存儲提交的內部方式非常快速地丟棄所有確切的重復文件——這很容易,因為早期的重復數據刪除。 然后它比較不同的文件並提出一組指令。 將這些指令應用於較早提交的文件會生成這些文件的后期提交副本。 這是一個差異,這就是我們喜歡查看提交的方式。 但這不是它們的存儲方式:Git 每次我們尋找一個新的差異時都會產生一個新的差異。
我們不必比較相鄰的提交。 例如,我們可以比較 commit F
與 commit H
,看看從F
到H
的變化。 使用git diff
命令,我們可以比較我們喜歡的任何兩個提交。 Git 將比較快照並為我們提供將其中一個更改為另一個的秘訣。 如果願意,我們可以向后進行比較,以獲得將H
變回G
的方法(“反向差異”)。 所有提交都有一個快照,因此您可以選擇任意兩個提交並以這種方式進行比較。
到目前為止提出的所有概念都不是很難。 當您打開每一個並查看內部時,它們有很多棘手的部分,但概念很簡單。 至少在高層次上理解它們是至關重要的,這樣我們才能跳入git merge
的工作原理,以實現真正的合並。
假設我們有以下一系列提交,以兩個分支提示提交結束:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
我們想運行git checkout br1
然后git merge br2
,這里的想法- 這個操作的目標- 是將兩個分支中完成的工作結合起來。 此外,我們希望Git盡可能多地進行這種組合。
現在,提交J
只有一個快照,加上通常的元數據。 提交L
也是如此。 我們可以使用git diff
將J
中的快照與L
中的快照進行比較,但這不會很好地工作。 假設某些文件在J
vs L
中有所不同:
This is
quite
a file.
與:
This is
not
a file.
比較這個文件的J
和L
副本只是告訴我們它們是不同的。 我們對分支機構所做的任何工作一無所知。
使用提交I
和K
怎么樣? 好吧,如果我們將I
中的快照與J
中的快照進行比較,這可以告訴我們在br1
中所做的事情。 所以這似乎至少好一點。 但是,在提交I
中完成的使該快照與提交H
不同的事情呢? 這也是br1
中的“工作完成”。 所以我們最好 go 一直回到H
。
同時,同樣的 arguments 申請提交L
, K
和H
。 我們需要 go 一直回到H
,然后才能開始看到“在分支br2
中完成的工作”。 提交H
出現在這兩個中。 提交H
有什么特別之處? 對此稍加思考。 再看這張圖:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
如果您說特別之處在於 commit H
在兩個分支上,那您是對的。 當然,提交G
以及G
之前的任何提交也是如此。 我們實際上可以利用這些,但G
-vs- H
已經在“兩個分支”上,通過H
中的快照。 因此,沒有理由將 go 進一步退回。 H
是停止的正確位置:它是兩個分支上最好的共享提交。
Git 將此稱為最佳共享提交合並基礎。 像本例一樣,只有一個合並基礎是非常常見的,盡管有兩個或更多的復雜情況。 我們不會在這里擔心這些情況,但如果您想探索所有這些背后的理論,請查看有向無環圖的最低公共祖先。 如果您使用 Git 並想查找LCA,請運行git merge-base --all br1 br2
例如,查看這些提交 Z0800FC577294C34E0B28AD283943594。 在這種特殊情況下,您將只獲得一個提交 hash 提交H
的 ID,因此這是此合並的合並基礎。
現在, Git可以比較H
與I
,然后I
與J
。 但實際上,它不需要這樣做。 4所以它只是直接比較H
和J
,就好像通過運行:
git diff --find-renames <hash-of-H> <hash-of-J>
然后它對H
-vs- L
做同樣的事情,另一個git diff --find-renames
和兩個 hash ID:
git diff --find-renames <hash-of-H> <hash-of-L>
這產生了兩個配方,用於將H
中的快照分別更改為J
和L
中的快照。 合並過程現在很簡單:我們查看每個配方中需要完成的操作,並結合任何單獨的更改。 如果H
-vs- J
說什么都不做:
This is
not
a file.
文件,但H
-vs- L
表示將中間行更改為 read quiet , quite
組合就是進行該更改。
Git 將這些組合指令應用於在提交H
中找到的快照。 這樣,我們保留了我們在br1
上所做的所有工作——請記住,我們從git checkout br1
開始——並添加他們——無論他們是誰——在br2
上所做的所有工作。
4 Git 處理文件重命名的方式很棘手,實際上可能受益於從合並基礎到分支提示的逐個提交掃描。 不過,Git 目前不這樣做。
合並(例如通過運行git merge br2
執行)是一個相當長且復雜的過程。 我們首先選擇我們想要“打開”的分支,使用git checkout br1
:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
這里括號中的HEAD
向我們展示了我們“打開”的分支。 我們所做的任何新提交都將在同一分支“上”,並將導致 Git 將新提交的 hash ID 寫入分支名稱。
然后git merge
命令做了一堆分析。 這包括查找合並基礎提交(在本例中為提交H
)並決定是否實際執行計算合並結果的工作。 git merge
命令的一些選項可以幫助控制這一點。 在我們的特殊情況下, git merge
無論如何都必須進行真正的合並,所以我們不需要任何選項來讓 Git 完成這項工作。 5
這意味着 Git 開始了合並過程,或者我喜歡將其稱為merge 作為動詞。 它找到了提交H
並確定需要真正的合並來組合H
-vs- J
和H
-vs- L
。 它在內部生成兩個差異,以確定如何進行這種組合。 然后它查看每個要合並的文件以實際進行組合。
如果這里出現問題,Git 將停止合並沖突,讓我們清理混亂。 這樣的合並仍在進行中,但沒有 Git 命令正在運行:它們已將所有內容記錄在文件中並退出。 您現在必須使用單獨的修復命令6來修復每個沖突; 這些記錄你所說的是正確的結果。 錄制完所有內容后,運行git merge --continue
以獲取合並以讀取記錄並完成合並。
但是,假設沒有任何問題, Git 將自行完成所有工作組合,並自行完成最終合並提交,無需git merge --continue
。 結果是提交。 像每個提交一樣,它有一個快照和元數據。 唯一特別的是元數據列出了兩個先前的提交。 這就是使它成為合並提交(作為形容詞合並)或合並的原因,使用單詞merge作為名詞。 讓我們畫出結果:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
請注意,名稱br1
現在像往常一樣指向新提交。 這個新的合並——merge作為名詞——的唯一特別之處在於它不僅指向提交J
,就像任何其他提交一樣,而且指向提交L
,這就是它成為合並提交的原因。 合並為動詞過程使此合並成為名詞/形容詞提交,這讓我們知道br2
現在已合並到br1
: M
有L
作為父級,而br2
指向L
,因此不再需要名稱br2
並且如果我們願意,可以立即刪除。
5如果合並基礎是當前提交並且另一個分支提示提交“領先於”這個提交, git merge
可以並且默認情況下會執行快進操作而不是合並。 您可以強制 Git 在此處使用git merge --no-ff
進行真正的合並。 如果合並基礎是另一個提交,並且與此提交相同,或者在此提交“之后”,則無法進行合並,並且git merge
將Already up to date
並退出。 如果兩個提交不相關——因此根本沒有合並基礎git merge
將抱怨歷史不相關,然后退出; --allow-unrelated-histories
選項使其無論如何都進行合並,使用空樹作為假合並基礎。 -n
/ --no-commit
和-s
/ --squash
選項防止 Git 進行新的提交,而-s
選項使 Git 在合並停止時“忘記”合並。 不過,我們不會在此處詳細介紹這些選項的任何細節。
6這可以包括在工作樹文件上運行您的編輯器並使用git add
,或者您可以使用git mergetool
,如果您喜歡它(我不喜歡),或者您喜歡的任何其他程序。 Git 不會強迫您使用任何特定的方法,但它確實相信您得到正確的合並結果。 它不檢查,它只是假設你所做的一切都是正確的!
現在我們有了:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
我們可以簡單地刪除名稱br2
。 分支名稱的目的是找到某個提交(分支的提示提交),如果不需要專門找到L
,我們可以通過從M
開始並向后工作來找到它。 但是如果我們願意,我們可以繼續在br2
上進行新的提交,方法是:
git checkout br2
然后做工作並運行git commit
。 結果可能如下所示:
I--J
/ \
...--G--H M <-- br1
\ /
K--L--N--O--P <-- br2 (HEAD)
此時, br2
上的提交只能通過從P
開始——通過名稱br2
並向后工作,所以現在我們不能刪除名稱br2
。
此時,我們也可以運行git checkout br1
然后git merge br2
。 合並將再次找到最佳共享提交。 這一次,這實際上是提交L
。
合並為動詞的過程將首先比較L
-vs- M
,看看“我們”在br1
上做了什么,然后比較L
-vs- P
,看看“他們”在br2
上做了什么。 然后 Git 將嘗試組合這些更改,將它們應用於L
中的快照。 這將保留M
上的“我們的”更改——我們通過生成M
的合並引入的那些——並在P
上添加“他們的”更改,如果可行,我們將獲得一個新的合並提交M2
或Q
或任何我們想叫它:
I--J
/ \
...--G--H M--------M2 <-- br1 (HEAD)
\ / /
K--L--N--O--P <-- br2
此時,我們可以安全地(再次)刪除名稱br2
,或者保留(再次)我們喜歡的名稱。
合並是關於合並工作——更改——但 Git 不存儲更改。 查找更改的唯一方法是比較一些特定的提交。 這意味着如果你想知道合並會做什么,或者為什么合並會這樣做,你必須比較各種提交:
git merge-base --all
找到合並基礎。git diff --find-renames
基礎和分支提示。 您所看到的這兩組差異是合並的輸入。 為合並提供的任何選項,例如-s ours
或-X ours
,都會影響更改的組合方式。 請注意-s ours
意味着完全忽略他們的更改。 不幸的是, git merge
在最終合並提交中沒有記錄用於生成合並的任何選項。 (我認為這是一個錯誤:它確實應該在合並提交的元數據中。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.