[英]How does git pull manage commit history?
假設我克隆了一個遠程存儲庫,到目前為止它有 1 個提交 => A
。 然后,我對我的本地分支進行了兩次提交,因此它變為 => A - B - C
。 但是,我的同事同時向他們的本地分支提交了另外兩次提交,因此他們的提交歷史記錄變為 => A - D - E
。 然后他們將其推送到遠程存儲庫。
然后我意識到我想推送我的更改,但是git push
告訴我遠程存儲庫在我前面。 所以,我做git pull
。
我的問題是,現在跟蹤遠程跟蹤分支的本地分支是什么樣的? 我知道會有合並沖突,但我的實際問題是:提交歷史會是什么樣子?
更具體地說,假設我修復了沖突並現在提交,我的提交歷史會像這樣A - D - E - F
還是A - B - C - D - E - F
? git 中的提交歷史是否是非線性的?
是的,如果它是一個正常的拉動,就會有 2 個父分支的合並......所以你會有兩個平行的分支,比如說。 使用git log --all --graph
查看結果。 作為旁注:沖突不是強制性的。 它們出現的原因有很多,但在您合並時並不是給定的。 您可能會經常進行無沖突的合並。
最短的答案(不是 100% 准確,但非常接近)是git pull
根本不管理歷史記錄。 git pull
為你做的是運行兩個 Git 命令,作為初學者,我建議你單獨運行:
首先, git pull
執行git fetch
。 這個命令非常簡單明了(但有一些曲折)。 它從其他一些存儲庫獲取新的提交:您的 Git 調用其他一些 Git,您的 Git 和他們的 Git 交換提交哈希 ID,並且由此,您的 Git 發現您需要從他們那里獲取哪些提交(和相關文件),以便您將通過 Internet 獲得合理最少的數據量來完成他們的所有提交。
完成后, git pull
運行第二個 Git 命令。 這是最復雜的地方。 (這些第二個命令往往有很多選項、模式和功能,所以它幾乎就像運行十幾個命令中的一個一樣。)
第二個 Git 命令的選擇是你的,但是當你使用git pull
,你必須在你有機會看到git fetch
會做什么之前做出選擇。 我認為這是不好的(大寫 B 不好,但不是粗體或斜體不好,所以只是中等不好😀)。 一旦你經常使用 Git,並且知道 fetch 是如何工作的,也許更重要的是,已經發現某些同事或同事或朋友如何使用 Git——這些都會影響git fetch
將做什么——可以安全地決定如何在獲取它們之前集成獲取的提交。 但在早期,我認為問得有點過分。 1
1總是可以撤消第二個命令所做的事情,但您需要了解第二個命令的所有信息。 作為初學者,您甚至可能沒有意識到這里有兩個不同的命令。 您當然不會知道足夠多,無法撤消每個命令的每個模式的每個效果。
git fetch
之后你有正確的設置假設我克隆了一個遠程存儲庫,到目前為止它有 1 個提交 =>
A
。 然后,我對我的本地分支進行了兩次提交,因此它變為 =>A - B - C
。 但是,我的同事同時向他們的本地分支提交了另外兩次提交,因此他們的提交歷史記錄變為 =>A - D - E
。 然后他們將 [this] 推送到 [共享遠程] 存儲庫。
當他們打你沖他們git push
到在共享第三收納庫的共享(三)資源庫“勝”,在提交現在有ADE
形式:
A--D--E <-- main
(這里的分支名稱並不是那么重要,但我使用的是main
因為 GitHub 現在使用它作為默認值,並且您在標簽中提到了github 。)
git fetch
步驟得到的是提交D
和E
。 您已經提交了A
,並且在提交后無法更改任何提交。 2所以你只需要DE
,它會像這樣在你的存儲庫中結束:
B--C <-- main
/
A
\
D--E <-- origin/main
名稱origin/main
是您的 Git 的遠程跟蹤名稱,您的 Git 根據其 Git 的分支名稱main
創建該名稱。 您的 Git 獲取每個 Git 的分支名稱並對其進行更改,以生成這些遠程跟蹤名稱。 由於遠程跟蹤名稱不是分支名稱,因此git fetch
對它們所做的任何更改(用於處理其他 Git 存儲庫中發生的任何事情)都不會影響您的任何分支。 因此運行git fetch
總是安全的。 3
我在自己的行上繪制了提交A
以強調它只是一個提交,由兩條開發線共享。 並且——需要考慮——如果一個分支是一條開發線,那么origin/main
不就是一個分支嗎? 這是“分支”的模糊定義, 4但它很快就會派上用場。
2請注意,例如, git commit --amend
並不會實際更改提交。 相反,它進行了一個新的提交,並讓您使用它而不是您正在使用的另一個提交。 您現在有兩個幾乎相同的提交,其中一個只是被推到一邊並被忽略。
3你可以設置git fetch
,或者給它參數,讓它做“不安全”的事情,但這很難。 通常的簡單方法是制作鏡像克隆,但鏡像克隆也會自動--bare
,並且裸克隆不會讓您在其中做任何工作。 (鏡像克隆僅用於特殊情況,不適用於普通的日常工作。)
4 Git 對分支的定義故意弱化和模糊,小心說出分支名稱會有所幫助。 分支名稱定義明確,不會受到這種哲學歧義的影響。 遠程跟蹤名稱與分支名稱明顯不同,盡管這兩種名稱都可以讓 Git 找到提交,並且提交本身形成了我們(人類)喜歡認為的“分支”。 所以從這個意義上說, origin/main
是一個可以找到分支的名稱。 它只是不是分支名稱:在內部,它拼寫為refs/remotes/origin/main
,其中分支名稱必須以refs/heads/
開頭。 分支名稱main
在內部拼寫為refs/heads/main
。 另請參閱“分支”究竟是什么意思?
git merge
或git rebase
git pull
運行的第二個命令是大部分實際操作發生的地方。 這要么是git merge
,要么是git rebase
。 5這些處理您使用git fetch
設置的分歧。 每個人都使用不同的方法。
合並從根本上比變基簡單。 這是因為 rebase 通過復制提交來工作,就像通過運行git cherry-pick
——某些形式的git rebase
字面上使用git cherry-pick
而其他形式使用近似值——並且每個cherry-pick 本身就是一種合並。 這意味着,例如,當您對三個提交進行變基時,您將執行三個合並。 rebase 執行的復制之后是一個內部 Git 操作,而許多形式的git merge
是一步完成的。
5從技術上講, git pull
可以在一種特殊情況下運行git checkout
,但這種情況不適用於這里。
從根本上說,合並是關於合並工作。
請注意,當我們遇到類似上面繪制的情況時,我們必須合並工作,其中一些共同的起點(提交A
)之后是不同的工作。 然而,在某些情況下,“組合工作”是微不足道的:
A <-- theirs
\
B--C <-- ours
在這里,“他們”——不管他們是誰——實際上並沒有做任何工作,所以要將你的工作與他們的工作“結合”,你可以讓 Git 切換到你最新的提交:
A--B--C <-- (combined successfully)
Git 將這種“組合”稱為快進操作,當git merge
執行此操作時,Git 將其稱為快進合並。 一般來說,如果git merge
可以做一個快進合並,它就會做一個。 如果沒有,它將進行全面合並。
一個完整的合並找到一個合並基礎——一個在兩個分支上的共享提交,使用我之前提到的故意寬松的分支定義,並將該特定提交中的快照與兩個分支提示提交中的快照進行比較。 這讓 Git 能夠弄清楚“我們改變了什么”以及“他們改變了什么”:
B--C <-- main
/
A
\
D--E <-- origin/main
從A
到C
的差異顯示了我們在兩次提交中所做的更改。 從DIFF A
以E
顯示了他們在兩次提交更改。
然后,Git 嘗試將兩組更改合並並應用到提交A
的快照。 如果 Git 認為這一切順利,Git 將繼續根據結果制作一個新的快照——一個新的提交。 通過接受我們的更改並添加他們的更改(或者,等效地,接受他們的更改並添加我們的更改),Git 的合並提交將具有作為其快照的 ?correct? 組合。 這里的問號是因為 Git 只是使用簡單的逐行規則。 結果在其他意義上可能不正確:它只是按照 Git 規則正確。
在任何情況下,新的合並提交了Git將使現在連回了我們的當前提交C
和他們的承諾E
:
B--C
/ \
A F <-- main
\ /
D--E <-- origin/main
我們的分支名稱main
現在選擇新的合並提交F
。 請注意, F
有一個快照,就像任何普通提交一樣,以及日志消息和作者等,就像任何普通提交一樣。 唯一特別的F
是不是指回一個先前的承諾,它指向回到二。
但是,這會產生巨大的后果,因為 Git查找提交的方式是從某個名稱開始——通常是一個分支名稱,盡管任何類型的名稱都可以——並使用它來定位最后一次提交,然后按照所有向后鏈接到所有以前的提交。 因此,從F
,Git “同時”退回到C
和E
6
6由於這不太可能,Git 必須使用某種近似值。 Git 的某些部分使用廣度優先搜索算法,而其他部分使用各種技巧。
從根本上講,rebase 是關於接受一些“還可以,但不夠好”的提交,並將它們復制到(據說)更好的新的和改進的提交中,然后放棄原始提交以支持新的和-改進的副本。
這樣做有幾個問題:
Git“喜歡”添加新的提交。 它“不喜歡”扔掉舊的提交。 Rebase 強制 Git 拋棄舊的,轉而支持新的和改進的,就目前而言這很好,但是......
我們將提交從一個 Git 存儲庫發送到另一個。 一旦它們被復制——一旦馬匹從谷倉中出來並被克隆——摧毀它們中的一些是沒有好處的。 如果我們有新的和改進的替代品,我們必須讓每個擁有原始副本的 Git 都拿起並切換到新的和改進的替代品。 這意味着我們需要強制其他 Git 放棄一些現有的提交。
一條始終有效的簡單規則是:只替換您從未放棄的提交。 這是有效的,因為如果你有唯一的副本,你的新的和改進的替代品不需要讓任何其他Git 扔掉舊的。 不涉及其他 Git 存儲庫! 但這太簡單了,至少有許多 GitHub 工作流。
一種更復雜的處理方法是:只有您和這些存儲庫的所有其他用戶事先同意的替換提交才能被替換。 其他用戶會——如果他們正在注意,至少——會注意到替代品並拿起它們。
沒有深入了解所有細節, git rebase
作用是:
git cherry-pick
或其他等效工具,一一復制要復制的提交; 最后在這種情況下,您可以將您的兩個現有提交變基(復制)為兩個新的和改進的提交:
B--C <-- main
/
A B'-C' <-- HEAD
\ /
D--E <-- origin/main
其中B'
和C'
是B
和C
的副本。 B'
的快照是通過對E
的快照進行更改來構建的; 要進行的更改是通過比較A
和B
看到的更改。 C'
的快照與此類似,但通過將B
更改為C
。
一旦副本全部完成,Git 將舊的main
標簽從舊的C
提交上剝離,並將其粘貼到新的C'
提交上:
B--C [abandoned]
/
A B'-C' <-- main (HEAD)
\ /
D--E <-- origin/main
最初的B
和C
提交仍然存在一段時間,但沒有一種簡單的方法可以找到它們,你只是看不到它們了。 如果您沒有仔細記下原始B
和C
的真實哈希 ID,您會認為它們的新的和改進的替換以某種方式神奇地改變了B
和C
的位置。 但他們沒有:他們是全新的,並且舊的提交仍然存在。 舊的提交根本沒有使用。 一段時間后——默認情況下至少 30 天——Git 會認為它們是垃圾,最終,用git gc
(Git 自動為你運行,通過git gc --auto
從沒有你的情況下從各種 Git 命令中分離出來)“垃圾收集”它們必須做任何事情)。
如果一切順利,重新定位的提交“保留了你工作的本質”,讓你看起來好像在你看到你的同事要做什么之后才開始工作。 復制提交中的日期和時間戳雖然更復雜:
您可以重復重新提交提交,並且每個副本中都會保留作者時間戳。 例如,要查看兩個時間戳,請使用git log --pretty=fuller
。
提交歷史不必是線性的。 假設您的朋友對某個文件進行了更改並將其推送。 所以遙遠的歷史看起來像A - D - E
。 如果您進行了一些其他更改,使您的提交歷史記錄為A - B - C
,那么如果存在沖突並且您修復了這些沖突並將提交推送到遠程,則遠程歷史記錄將如下所示:
/--D---E-\
A P
\--B---C-/
這里P
是提交您解決沖突的地方。 (解決沖突基本上是使用已解決的更改進行新的提交。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.