解讀那些令人困惑 Git 術語
![](https://linuxstory.org/wp-content/uploads/2024/06/200630j90z4xz7tttgztr9-800x400.jpg)
我正在一步步解釋 Git 的方方面面。在使用 Git 近 15 年後,我已經非常習慣於 Git 的特性,很容易忘記它令人困惑的地方。
因此,我在 Mastodon 上進行了調查:
你有覺得哪些 Git 術語很讓人困惑嗎?我計劃寫篇博客,來解讀 Git 中一些奇怪的術語,如:「分離的 HEAD 狀態」,「快速前移」,「索引/暫存區/已暫存」,「比
origin/main
提前 1 個提交」等等。
我收到了許多有洞見的答案,我在這裡試圖概述其中的一部分。下面是這些術語的列表:
- HEAD 和 「heads」
- 「分離的
HEAD
狀態」 - 在合併或變基時的 「ours」 和 「theirs」
- 「你的分支已經與 'origin/main' 同步」
HEAD^
、HEAD~
、HEAD^^
、HEAD~~
、HEAD^2
、HEAD~2
..
和...
- 「可以快速前移」
- 「引用」、「符號引用」
- refspecs
- 「tree-ish」
- 「索引」、「暫存的」、「已緩存的」
- 「重置」、「還原」、「恢復」
- 「未跟蹤的文件」、「追蹤遠程分支」、「跟蹤遠程分支」
- 檢出
- reflog
- 合併、變基和遴選
rebase –onto
- 提交
- 更多複雜的術語
我已經儘力講解了這些術語,但它們幾乎覆蓋了 Git 的每一個主要特性,這對一篇博客而言顯然過於繁重,所以在某些地方可能會有一些粗糙。
HEAD
和 「heads」
有些人表示他們對 HEAD
和 refs/heads/main
這些術語感到困惑,因為聽起來像是一些複雜的技術內部實現。
以下是一個快速概述:
- 「heads」 就是 「分支」。在 Git 內部,分支存儲在一個名為
.git/refs/heads
的目錄中。(從技術上講,官方 Git 術語表 中明確表示分支是所有的提交,而 head 只是最近的提交,但這只是同一事物的兩種不同思考方式) HEAD
是當前的分支,它被存儲在.git/HEAD
中。
我認為,「head 是一個分支,HEAD
是當前的分支」 或許是 Git 中最奇怪的術語選擇,但已經設定好了,想要更清晰的命名方案已經為時已晚,我們繼續。
「HEAD 是當前的分支」 有一些重要的例外情況,我們將在下面討論。
「分離的 HEAD 狀態」
你可能已經看到過這條信息:
$ git checkout v0.1
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
[...]
(消息譯文:你處於 「分離 HEAD」 的狀態。你可以四處看看,進行試驗性的更改並提交,你可以通過切換回一個分支來丟棄這個狀態下做出的任何提交。)
這條信息的實質是:
- 在 Git 中,通常你有一個已經檢出的 「當前分支」,例如
main
。 - 存放當前分支的地方被稱為
HEAD
。 - 你做出的任何新提交都會被添加到你的當前分支,如果你運行
git merge other_branch
,這也會影響你的當前分支。 - 但是,
HEAD
不一定必須是一個分支!它也可以是一個提交 ID。 - Git 會稱這種狀態(
HEAD
是提交 ID 而不是分支)為 「分離的HEAD
狀態」 - 例如,你可以通過檢出一個標籤來進入分離的
HEAD
狀態,因為標籤不是分支 - 如果你沒有當前分支,一系列事情就斷鏈了:
git pull
根本就無法工作(因為它的全部目的就是更新你的當前分支)- 除非以特殊方式使用
git push
,否則它也無法工作 git commit
、git merge
、git rebase
和git cherry-pick
仍然可以工作,但它們會留下「孤兒」提交,這些提交沒有連接到任何分支,因此找到這些提交會很困難
- 你可以通過創建一個新的分支或切換到一個現有的分支來退出分離的
HEAD
狀態
在合併或變基中的 「ours」 和 「theirs」
遇到合併衝突時,你可以運行 git checkout --ours file.txt
來選擇 「ours」 版本中的 file.txt
。但問題是,什麼是 「ours」,什麼是 「theirs」 呢?
我總感覺此類術語混淆不清,也因此從未用過 git checkout --ours
,但我還是查找相關資料試圖理清。
在合併的過程中,這是如何運作的:當前分支是 「ours」,你要合併進來的分支是 「theirs」,這樣看來似乎很合理。
$ git checkout merge-into-ours # 當前分支是 「ours」
$ git merge from-theirs # 我們正要合併的分支是 「theirs」
而在變基的過程中就剛好相反 —— 當前分支是 「theirs」,我們正在變基到的目標分支是 「ours」,如下:
$ git checkout theirs # 當前分支是 「theirs」
$ git rebase ours # 我們正在變基到的目標分支是 「ours」
我以為之所以會如此,因為在操作過程中,git rebase main
其實是將當前分支合併到 main
(它類似於 git checkout main; git merge current_branch
),儘管如此我仍然覺得此類術語會造成混淆。
這個精巧的小網站 對 「ours」 和 「theirs」 的術語進行了解釋。
人們也提到,VSCode 將 「ours」/「theirs」 稱作 「當前的更改」/「收到的更改」,同樣會引起混淆。
「你的分支已經與 origin/main
同步」
此信息貌似很直白 —— 你的 main
分支已經與源端同步!
但它實際上有些誤導。可能會讓你以為這意味著你的 main
分支已經是最新的,其實不然。它真正的含義是 —— 如果你最後一次運行 git fetch
或 git pull
是五天前,那麼你的 main
分支就是與五天前的所有更改同步。
因此,如果你沒有意識到這一點,它對你的安全感其實是一種誤導。
我認為 Git 理論上可以給出一個更有用的信息,像是「與五天前上一次獲取的源端 main
是同步的」,因為最新一次獲取的時間是在 reflog 中記錄的,但它沒有這麼做。
HEAD^
、HEAD~
、HEAD^^
、HEAD~~
、HEAD^2
、HEAD~2
我早就清楚 HEAD^
代表前一次提交,但我很長一段時間都困惑於 HEAD~
和 HEAD^
之間的區別。
我查詢資料,得到了如下的對應關係:
HEAD^
和HEAD~
是同一件事情(指向前 1 個提交)HEAD^^^
、HEAD~~~
和HEAD~3
是同一件事情(指向前 3 個提交)HEAD^3
指向提交的第三個父提交,它與HEAD~3
是不同的
這看起來有些奇怪,為什麼 HEAD~
和 HEAD^
是同一個概念?以及,「第三個父提交」是什麼?難道就是父提交的父提交的父提交?(劇透:並非如此)讓我們一起深入探討一下!
大部分提交只有一個父提交。但是合併提交有多個父提交 - 因為它們合併了兩個或更多的提交。在 Git 中,HEAD^
意味著 「HEAD 提交的父提交」。但是如果 HEAD
是一個合併提交,那 HEAD^
又代表怎麼回事呢?
答案是,HEAD^
指向的是合併提交的第一個父提交,HEAD^2
是第二個父提交,HEAD^3
是第三個父提交,等等。
但我猜他們也需要一個方式來表示「前三個提交」,所以 HEAD^3
是當前提交的第三個父提交(如果當前提交是一個合併提交,可能會有很多父提交),而 HEAD~3
是父提交的父提交的父提交。
我想,從我們之前對合併提交 「ours」/「theirs」 的討論來看,HEAD^
是 「ours」,HEAD^2
是 「theirs」。
..
和 ...
這是兩個命令:
git log main..test
git log main...test
我從沒用過 ..
和 ...
這兩個命令,所以我得查一下 man git-range-diff。我的理解是比如這樣一個情況:
A - B main
C - D test
main..test
對應的是提交 C 和 Dtest..main
對應的是提交 Bmain...test
對應的是提交 B,C,和 D
更有挑戰的是,git diff
顯然也支持 ..
和 ...
,但它們在 git log
中的意思完全不同?我的理解如下:
git log test..main
顯示在main
而不在test
的更改,但是git log test...main
則會顯示 兩邊 的改動。git diff test..main
顯示test
變動 和main
變動(它比較B
和D
),而git diff test...main
會比較A
和D
(它只會給你顯示一邊的差異)。
有關這個的更多討論可以參考 這篇博客文章。
「可以快速前移」
在 git status
中,我們會經常遇到如下的信息:
$ git status
On branch main
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
(消息譯文:你現在處於 main
分支上。你的分支比 origin/main
分支落後了 2 個提交,可以進行快速前進。 (使用 git pull
命令可以更新你的本地分支))
但「快速前移」 到底是何意?本質上,它在告訴我們這兩個分支基本如下圖所示(最新的提交在右側):
main: A - B - C
origin/main: A - B - C - D - E
或者,從另一個角度理解就是:
A - B - C - D - E (origin/main)
|
main
這裡,origin/main
僅僅多出了 2 個 main
不存在的提交,因此我們可以輕鬆地讓 main
更新至最新 —— 我們所需要做的就是添加上那 2 個提交。事實上,這幾乎不可能出錯 —— 不存在合併衝突。快速前進式合併是個非常棒的事情!這是合併兩個分支最簡單的方式。
運行完 git pull
之後,你會得到如下狀態:
main: A - B - C - D - E
origin/main: A - B - C - D - E
下面這個例子展示了一種不能快速前進的狀態。
A - B - C - X (main)
|
- - D - E (origin/main)
此時,main
分支上有一個 origin/main
分支上無的提交(X
),所以無法執行快速前移。在此種情況,git status
就會如此顯示:
$ git status
Your branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.
(你的分支和 origin/main
分支已經產生了分歧,其中各有 1 個和 2 個不同的提交。)
「引用」、「符號引用」
在使用 Git 時,「引用」 一詞可能會使人混淆。實際上,Git 中被稱為 「引用」 的實例至少有三種:
- 分支和標籤,例如
main
和v0.2
HEAD
,代表當前活躍的分支- 諸如
HEAD^^^
這樣的表達式,Git 會將其解析成一個提交 ID。確切說,這可能並非 「引用」,我想 Git 將其稱作 「版本參數」,但我個人並未使用過這個術語。
個人而言,「符號引用」 這個術語頗為奇特,因為我覺得我只使用過 HEAD
(即當前分支)作為符號引用。而 HEAD
在 Git 中佔據核心位置,多數 Git 核心命令的行為都基於 HEAD
的值,因此我不太確定將其泛化成一個概念的實際意義。
refspecs
在 .git/config
配置 Git 遠程倉庫時,你可能會看到這樣的代碼 +refs/heads/main:refs/remotes/origin/main
。
[remote "origin"]
url = git@github.com:jvns/pandas-cookbook
fetch = +refs/heads/main:refs/remotes/origin/main
我對這段代碼的含義並不十分清楚,我通常只是在使用 git clone
或 git remote add
配置遠程倉庫時採用默認配置,並沒有動機去深究或改變。
「tree-ish」
在 git checkout
的手冊頁中,我們可以看到:
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...
那麼這裡的 tree-ish
是什麼意思呢?其實當你執行 git checkout THING .
時,THING
可以是以下的任一種:
- 一個提交 ID(如
182cd3f
) - 對一個提交 ID 的引用(如
main
或HEAD^^
或v0.3.2
) - 一個位於提交內的子目錄(如
main:./docs
) - 可能就這些?
對我個人來說,「提交內的目錄」這個功能我從未使用過,從我的視角看,tree-ish
可以解讀為「提交或對提交的引用」。
「索引」、「暫存」、「緩存」
這些術語都指向的是同一樣東西(文件 .git/index
,當你執行 git add
時,你的變動會在這裡被暫存):
git diff --cached
git rm --cached
git diff --staged
- 文件
.git/index
儘管它們都是指向同一個文件,但在實際使用中,這些術語的應用方式有所不同:
- 很顯然,
--index
和--cached
並不總是表示同一種意思。我自己從未使用--index
,所以具體細節我就不展開討論了,但是你可以在 Junio Hamano(Git 的主管維護者)的博客文章 中找到詳細解釋。 - 「索引」 會包含未跟蹤的文件(我猜可能是對性能的考慮),但你通常不會把未跟蹤的文件考慮在「暫存區」內。
「重置」、「還原」、「恢復」
許多人提到,「 重置 」、「 還原 」 和 「 恢復 」 這三個詞非常相似,易使人混淆。
我認為這部分的困惑來自以下原因:
git reset --hard
和git restore .
單獨使用時,基本上達到的效果是一樣的。然而,git reset --hard COMMIT
和git restore --source COMMIT .
相互之間是完全不同的。- 相應的手冊頁沒有給出特別有幫助的描述:
git reset
: 「重置當前HEAD
到指定的狀態」git revert
: 「還原某些現有的提交」git restore
: 「恢復工作樹文件」
雖然這些簡短的描述為你詳細說明了哪個名詞受到了影響(「當前 HEAD
」,「某些提交」,「工作樹文件」),但它們都預設了你已經知道在這種語境中,「重置」、「還原」和「恢復」的準確含義。
以下是對它們各自功能的簡要說明:
- 重置 ——
git revert COMMIT
: 在你當前的分支上,創建一個新的提交,該提交是COMMIT
的「反向」操作(如果COMMIT
添加了 3 行,那麼新的提交就會刪除這 3 行)。 - 還原 ——
git reset --hard COMMIT
: 強行將當前分支回退到COMMIT
所在的狀態,抹去自COMMIT
以來的所有更改。這是一個高風險的操作。 - 恢復 ——
git restore --source=COMMIT PATH
: 將PATH
中的所有文件回退到COMMIT
當時的狀態,而不擾亂其他文件或提交歷史。
「未跟蹤的文件」、「遠程跟蹤分支」、「跟蹤遠程分支」
在 Git 中,「跟蹤」 這個詞以三種相關但不同的方式使用:
- 「 未跟蹤的文件 」:在
git status
命令的輸出中可以看到。這裡,「未跟蹤」 意味著這些文件不受 Git 管理,不會被計入提交。 - 「 遠程跟蹤分支 」 例如
origin/main
。此處的「遠程跟蹤分支」是一個本地引用,旨在記住上次執行git pull
或git fetch
時,遠程origin
上main
分支的狀態。 - 我們經常看到類似 「分支
foo
被設置為跟蹤origin
上的遠程分支bar
」這樣的提示。
即使「未跟蹤的文件」和「遠程跟蹤分支」都用到了「跟蹤」這個詞,但是它們所在的上下文完全不同,所以沒有太多混淆。但是,對於以下兩種方式的「跟蹤」使用,我覺得可能會產生些許困擾:
main
是一個跟蹤遠程的分支origin/main
是一個遠程跟蹤分支
然而,在 Git 中,「跟蹤遠程的分支」 和 「遠程跟蹤分支」 是不同的事物,理解它們之間的區別非常關鍵!下面是對這兩者區別的一個簡單概述:
main
是一個分支。你可以在它上面做提交,進行合併等操作。在.git/config
中,它通常被配置為 「追蹤」 遠程的main
分支,這樣你就可以用git pull
和git push
來同步和上傳更改。origin/main
則並不是一個分支,而是一個「遠程跟蹤分支」,這並不是一種真正的分支(這有些抱歉)。你不能在此基礎上做提交。只有通過運行git pull
或git fetch
獲取遠程main
的最新狀態,才能更新它。
我以前沒有深入思考過這種模糊的地方,但我認為很容易看出為什麼它會讓人感到困惑。
簽出
簽出做了兩個完全無關的事情:
git checkout BRANCH
用於切換分支git checkout file.txt
用於撤銷對file.txt
的未暫存修改
這是眾所周知的混淆點,因此 Git 實際上已經將這兩個功能分離到了 git switch
和 git restore
(儘管你還是可以使用 checkout
,就像我一樣,在不願丟棄 15 年對 git checkout
肌肉記憶的情況下)。
再者,即使用了 15 年,我仍然記不住 git checkout main file.txt
用於從 main
分支恢復 file.txt
版本的命令參數。
我覺得有時你可能需要在 checkout
命令後面加上--
,幫助區分哪個參數是分支名,哪個是路徑,但我並未這麼使用過,也不確定何時需要這樣做。
參考日誌(reflog)
有很多人把 reflog
讀作 re-flog
,而不是 ref-log
。由於本文已經足夠長,我這裡不會深入討論參考日誌,但值得注意的是:
- 在 Git 中,「參考」 是一個泛指分支、標籤和
HEAD
的術語 - 參考日誌(「reflog」)則為你提供了一個參考歷次記錄的歷史追蹤
- 它是從一些極端困境中拯救出來的利器,比如說你不小心刪除了重要的分支
- 我覺得參考日誌是 Git 用戶界面中最難懂的部分,我總是試圖避免使用它。
合併 vs 變基 vs 遴選
有許多人提及他們常常對於合併和變基的區別感到迷惑,並且不理解變基中的「 基 」指的是什麼。
我會在這裡盡量簡要的進行描述,但是這些一句話的解釋最終可能並不那麼明了,因為每個人使用合併和變基創建工作流程時的方式差別挺大,要真正理解合併和變基,你必須理解工作流程。此外,有圖示會更好理解。不過這個話題可能需要一篇獨立的博客文章來完整討論,所以我不打算深入這個問題。
- 合併會創建一個新的提交,用來融合兩個分支
- 變基則會逐個地把當前分支上的提交複製到目標分支
- 遴選跟變基類似,但是語法完全不同(一個顯著的差異是變基是從當前分支複製提交,而遴選則會把提交複製到當前分支)
rebase --onto
在 git rebase
中,存在一個被稱為 --onto
的選項。這一直讓我感到困惑,因為 git rebase main
的核心功能就是將當前分支變基到 main
運行上。那麼,額外的 --onto
參數又是怎麼回事呢?
我進行了一番查找,--onto
顯然解決了一個我幾乎沒有或者說從未遇到過的問題,但我還是會記錄下我對它的理解。
A - B - C (main)
D - E - F - G (mybranch)
|
otherbranch
設想一下,出於某種原因,我只想把提交 F
和 G
變基到 main
上。我相信這應該是某些 Git 工作流中會經常遇到的場景。
顯然,你可以運行 git rebase --onto main otherbranch mybranch
來完成這個操作。對我來說,在這個語法中記住 3 個不同的分支名順序似乎是不可能的(三個分支名,對我來說實在太多了),但由於我從很多人那裡聽說過,我想它一定有它的用途。
提交
有人提到他們對 Git 中的提交作為一詞雙義(既作為動詞也作為名詞)的用法感到困惑。
例如:
- 動詞:「別忘了經常提交」
- 名詞:「
main
分支上最新的提交」
我覺得大多數人應該能很快適應這個雙關的用法,但是在 SQL 資料庫中的「提交」用法與 Git 是有所不同,我認為在 SQL 資料庫中,「提交」只是作為一個動詞(你使用 COMMIT
來結束一個事務),並不作為名詞。
此外,在 Git 中,你可以從以下三個不同的角度去考慮一個 Git 提交:
- 表示當前每個文件狀態的快照
- 與父提交的差異
- 記錄所有先前提交的歷史
這些理解都是不錯的:不同的命令在所有的這些情況下都會使用提交。例如,git show
將提交視為一個差異,git log
把提交看作是歷史,git restore
則將提交理解為一個快照。
然而,Git 的術語並無太多助於你理解一個給定的命令正在如何使用提交。
更多令人困惑的術語
以下是更多讓人覺得混淆的術語。我對許多這些術語的意思並不十分清楚。
我自己也不是很理解的東西:
git pickaxe
(也許這是git log -S
和git log -G
,它們用於搜索以前提交的差異?)- 子模塊(我知道的全部就是它們並不以我想要的方向工作)
- Git 稀疏檢出中的 「cone mode」 (沒有任何關於這個的概念,但有人提到過)
人們提及覺得混淆,但我在這篇已經 3000 字的文章中略過的東西:
- blob、tree
- 「合併」 的方向
- 「origin」、「upstream」,「downstream」
push
和pull
並不是對立面fetch
和pull
的關係(pull = fetch + merge)- git porcelain
- 子樹
- 工作樹
- 暫存
- 「master」 或者 「main」 (聽起來它在 Git 內部有特殊含義,但其實並沒有)
- 何時需要使用
origin main
(如git push origin main
)vsorigin/main
人們提及感到困惑的 Github 術語:
- 「 拉取請求 」 (與 Gitlab 中的 「 合併請求 」 相比,人們似乎認為後者更清晰)
- 「壓扁併合並」 和 「變基併合並」 的作用 (在昨天我從未聽說過
git merge --squash
,我一直以為 「壓扁併合並」 是 Github 的特殊功能)
確實是 「每個 Git 術語」
我驚訝地發現,幾乎 Git 的每個其他核心特性都被至少一人提及為某種方式中的困惑。我對聽到更多我錯過的混淆的 Git 術語的例子也有興趣。
關於這個,有另一篇很棒的 2012 年的文章叫做《最困惑的 Git 術語》。它更多的討論的是 Git 術語與 CVS 和 Subversion 術語的關聯。
如果我要選出我覺得最令人困惑的 3 個 Git 術語,我現在會選:
head
是一個分支,HEAD
是當前分支- 「遠程跟蹤分支」 和 「跟蹤遠程的分支」 是不同的事物
- 「索引」、「暫存的」、「已緩存的」 全部指的同一件事
就這樣了!
在寫這些的過程中,我學到了不少東西。我了解到了一些新的關於Git的事實,但更重要的是,現在我對於別人說Git的所有功能和特性都引起困惑有了更深的理解。
許多問題我之前根本沒考慮過,比如我從來沒有意識到,在討論分支時,「跟蹤」這個詞的用法是多麼地特別。
另外,儘管我已經儘力做到準確無誤,但由於我涉獵到了一些我從未深入探討過的Git的角落,所以可能還是出現了一些錯誤。
(題圖:DALL-E/A/e1e5b964-5f32-41bb-811e-8978fb8556d4)
via: https://jvns.ca/blog/2023/11/01/confusing-git-terminology/
作者:Julia Evans 選題:lujun9972 譯者:ChatGPT 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive