[英]git checkout <commit-hash> vs git checkout branch
我在玩 git 並在這里感到困惑。
develop
分支的HEAD位於
235a6d8
當我做:
git checkout 235a6d8
從任何其他分支或從develop
分支,這讓我處於超然狀態。
我不知道為什么當我檢查這個分支上的最新提交時會發生這種情況。
當我做:
git checkout develop
我可以切換到正確的開發分支。
我沒有得到git checkout <commit-has>
和git checkout branchname
。
它們有何不同?
一個git checkout <commit-hash>
,准備在<commit>
之上工作,通過在它上面分離 HEAD(參見“ DETACHED HEAD”部分),並更新索引和工作樹中的文件。
當git checkout <branch>
進行切換時:它准備在<branch>
上工作,通過更新索引和工作樹中的文件以及將 HEAD 指向分支來切換到它。
這令人困惑。
Mark Longair在“為什么切換分支的 git 命令名為“ git checkout
”?
他還在 2012 年 5 月寫道:“ 最令人困惑的 git 術語”:
在 CVS 和 Subversion 中,“checkout”創建鏈接到該存儲庫的源代碼的新本地副本。
Git 中最接近的命令是“git clone
”。
然而,在 git 中,“git checkout
”用於完全不同的東西。
事實上,它有兩種截然不同的操作模式:
- 要切換 HEAD 以指向新的分支或提交,請使用 git checkout
<branch>
。 如果<branch>
是真正的本地分支,這將切換到該分支(即 HEAD 將指向引用名稱),或者如果它以其他方式解析為提交將分離 HEAD 並將其直接指向提交的對象名稱。- 用來自特定提交或索引的內容替換工作副本和索引中的一個或多個文件。
這在用法中可以看到:git checkout -- (update from the index)
和git checkout <tree-ish> --
(其中<tree-ish>
通常是提交)。在我的理想世界中,這兩種操作方式會有不同的動詞,它們都不是“
checkout
” 。
嗯......這就是為什么 Git 2.23(2019 年第三季度)將結帳拆分為:
git restore
更新工作樹(可能還有索引)git switch
可以切換分支,或者在需要時分離分支,以便將所有新提交添加到該分支的尖端。除了 VonC 的回答(以及即將在 Git 2.23 中進行的更改)之外,還有幾項值得注意。
因為git checkout
做了多種不同的事情,所以它本質上是令人困惑的。
git checkout
的工作之一是根據目標提交填充索引和工作樹。 只要允許和必要,它就會這樣做。
另一種方法是改變記錄在分支名稱HEAD
,或設置HEAD
作為一個分離的頭在指定的提交。 它會在必要時執行此操作(前提是第一部分允許結帳操作)。
對於git checkout
,它將根據您提供的分支名稱或提交說明符參數執行第二個操作。 也就是說,假設我們有一些 shell 變量$var
設置為一些非空但合理的詞:它可能設置為master
,或者master^{commit}
或a23456f
或origin/develop
或類似的東西。 無論如何,我們現在運行:
git checkout $var
什么名稱或哈希 ID進入HEAD
? 好吧,這是git checkout
決定的方式:
首先, git checkout
嘗試解析我們剛剛作為分支名稱提供的字符串。 假設我們給它master
或develop
。 這是一個有效的現有分支嗎? 如果是這樣,這就是應該進入HEAD
的名稱。 如果結帳成功,我們會將分支切換到該分支。
否則,我們剛剛給它的字符串畢竟不是分支名稱(即使它以 1 開頭,例如在master~1
中)。 Git 會嘗試——嘗試——將其解析為提交哈希 ID,就像通過git rev-parse
。 例如, a23456f
確實看起來像一個縮寫的哈希 ID。 如果它是一個——如果 Git 的數據庫中有一個對象的 ID 以a23456f
——那么 Git 會確保這個 ID 命名一個commit ,而不是某個其他對象。 1如果它是一個提交哈希 ID,那么它應該作為一個分離的 HEAD 進入HEAD
的哈希 ID。 如果結帳成功,我們現在將在給定的提交處處於分離的 HEAD 模式。
如果這兩種嘗試都不起作用, git checkout
接下來會猜測$var
可能是一個文件名,並嘗試解決這個問題。 2但我們將在這里忽略這個特殊情況。
許多不是分支名稱的名稱在這里都可以正常工作。 例如, origin/master
極有可能被解析為提交哈希 ID。 如果v2.1
是有效標簽,則v2.1
可以解析為提交哈希 ID。 在所有這些情況下——只要$var
結果不是分支名稱,但可以解析為提交哈希 ID—— git checkout
將嘗試對該提交哈希執行分離的 HEAD 簽出。
一旦git checkout
決定您要求檢查某個特定的提交,要么作為分支名稱粘貼到附加的 HEAD 中,要么作為提交哈希 ID 粘貼到分離的 HEAD 中,然后Git 開始確定是否允許這樣做. 這會變得非常復雜! 見結帳時,有對當前分支未提交更改另一個分支關於是否以及何時它允許詳細的注釋,並記住--force
告訴Git的,它應該做的結賬無論如何,即使這些規則不會允許它。
但是,TL;DR 是原始哈希 ID始終是進入分離 HEAD 狀態的請求。 是否會導致分離的 HEAD 取決於復雜的“是否允許結帳”測試。
另請注意,如果您創建一個名稱可以是哈希 ID 的分支(例如cafedad
有時事情會cafedad
有點奇怪。 任何嘗試使用它作為分支名稱的Git 命令都會成功,因為它是一個。 任何嘗試將其用作短哈希 ID 的Git 命令都可能會成功,因為它可能是一個有效的短哈希 ID!
除非你創建愚蠢的令人困惑的分支名稱,否則這種特殊情況很少會成為問題,因為所有編寫良好的 Git 命令都會在短哈希 ID 之前嘗試分支名稱。 為了說明起見,我故意使用通過git log
找到的現有哈希的前六個字母創建了一個愚蠢的分支名稱:
$ git branch f9089e 8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git rev-parse f9089e
warning: refname 'f9089e' is ambiguous.
8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git branch -d f9089e
Deleted branch f9089e (was 8dca754b1e).
請注意警告: f9089e
被視為分支名稱,因為它解析為8dca754b1e874719a732bc9ab7b0e14b21b1bc10
。 刪除愚蠢的分支名稱后,短散列再次解析為完整散列:
$ git rev-parse f9089e
f9089e8491fdf50d941f071552872e7cca0e2e04
如果您創建的分支名稱意外地用作短哈希(例如babe
、 decade
或cafedad
, cafedad
您可能只在表示分支時輸入短名稱babe
或cafedad
。 如果您指的是提交,您可能會用鼠標或其他方式剪切並粘貼完整的哈希 ID。
當您創建具有相同名稱的分支和標記時,這里會發生真正的危險。 大多數Git 命令傾向於使用tag ,但git checkout
更喜歡branch 。 這是一個非常混亂的情況。 幸運的是,它很容易修復:只需重命名兩個實體之一,這樣您的分支名稱和標簽名稱就不會沖突。
(你也可以通過創建一個與某些現有完整哈希 ID 完全相同的分支名稱來惹惱自己。這個特別討厭,因為完整哈希 ID 往往優先於分支名稱,但同樣, git checkout
是一個例外這條規則。幸運的是git branch -d
也是如此。)
1任何 Git 存儲庫中都有四種類型的對象: commits 、 tree 、 blob和annotated tags 。 提交對象存儲提交。 樹和 blob 對象主要供 Git 內部使用,以某種類似於目錄的方式存儲文件名並存儲文件數據。 帶注釋的標簽對象是最棘手的:它們存儲另一個對象的哈希 ID。 可以指示 Git 獲取這樣的標簽並找到該標簽連接到的提交。 作為一種特殊的復雜情況,帶注釋的標簽最終會導致樹或 blob 對象,因此某些標簽可能根本不會命名提交——但通常,大多數標簽最終都會命名提交。
如果您使用git rev-parse
命令,您可以使用^{commit}
后綴技巧告訴 Git:確保最終對象的類型為 commit。 如果直接對象具有 annotated-tag 類型,Git 將“剝離”(跟隨其目的地)標簽以查找其提交。 如果它沒有找到提交——如果它找到了一棵樹或 blob—— git rev-parse
將吐出一條錯誤消息並使解析失敗。 如果您正在編寫自己的花哨腳本來對提交做一些有用的事情,那么這一切都被設計為正是所需要的。
(如果需要,這個“剝皮”過程會重復,因為一個帶注釋的標簽的目標可以是另一個帶注釋的標簽。這里的動詞peel是為了提醒一個人剝洋蔥:如果你發現另一層洋蔥,再剝。最終你會找出洋蔥的中心是什么。:-))
2請注意,從$var
到任何$var
設置的擴展是由shell (例如,通過 bash)完成的,而不是由 Git 完成的。 這在這里並不重要,因為我對$var
內容施加了限制,但在更復雜的情況下,它確實如此。
這是一個簡單的解釋:
HEAD 是一個位於.git/HEAD
的文件,它讓 Git 了解在新提交出現時應該推進的分支。
當一個分支,如main
被檢出時,它包含:
ref: refs/heads/main
對於每個分支,Git 還在refs/heads
目錄中保存一個文件,例如main
分支的文件refs/heads/main
將在那里。
該文件包含該分支上最后一次提交的哈希值,即分支的tip of the branch
。
到目前為止,這兩個文件告知 Git 應該推進哪個分支以及該分支上的最后一次提交是什么。
通過運行git checkout <branch name>
,HEAD 文件更新為包含該分支的名稱。
因此運行git checkout 235a6d8
,使 HEAD 指向特定的提交而不是指向特定的分支,這意味着 Git HEAD 是detached
。
為了再次附加 HEAD,只需運行git checkout <branch name>
這將使事情恢復正常行為。
分離的頭部狀態對於檢查特定時間點的項目狀態、測試和錯誤發現/修復非常有用。
您可以在分離的 HEAD 狀態下執行更多操作,您可以在文檔中看到更多相關信息。
您可能還會發現在gitglossary 中檢查術語heads
和HEAD
很有趣。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.