Linux中國

Git 分支:直覺與現實

你好!我一直在投入寫作一本關於 Git 的小冊,因此我對 Git 分支投入了許多思考。我不斷從他人那裡聽說他們覺得 Git 分支的操作方式違反直覺。這使我開始思考:直覺上的分支概念可能是什麼樣,以及它如何與 Git 的實際操作方式區別開來?

在這篇文章中,我想簡潔地討論以下幾點內容:

  • 我認為許多人可能有的一個直覺性的思維模型
  • Git 如何在內部實現分支的表示(例如,「分支是對提交的指針」)
  • 這種「直覺模型」與實際操作方式之間的緊密關聯
  • 直覺模型的某些局限性,以及為何它可能引發問題

本文無任何突破性內容,我會盡量保持簡潔。

分支的直觀模型

當然,人們對分支有許多不同的直覺。我自己認為最符合「蘋果樹的一個分支」這一物理比喻的可能是下面這個。

我猜想許多人可能會這樣理解 Git 分支:在下圖中,兩個紅色的提交就代表一個「分支」。

我認為在這個示意圖中有兩點很重要:

  1. 分支上有兩個提交
  2. 分支有一個「父級」(main),它是這個「父級」的分支

雖然這個觀點看似合理,但實際上它並不符合 Git 對於分支的定義 — 最重要的是,Git 並沒有一個分支的「父級」的概念。那麼,Git 又是如何定義分支的呢?

在 Git 里,分支是完整的歷史

在 Git 中,一個分支是每個過去提交的完整歷史記錄,而不僅僅是那個「分支」提交。因此,在我們上述的示意圖中,所有的分支(mainbranch)都包含了 4 次提交。

我創建了一個示例倉庫,地址為:https://github.com/jvns/branch-example。它設置的分支方式與前圖一樣。現在,我們來看看這兩個分支:

main 分支包含了 4 次提交:

$ git log --oneline main
70f727a d
f654888 c
3997a46 b
a74606f a

mybranch 分支也有 4 次提交。最後兩次提交在這兩個分支里都存在。

$ git log --oneline mybranch
13cb960 y
9554dab x
3997a46 b
a74606f a

因此,mybranch 中的提交次數為 4,而不僅僅是 2 次「分支」提交,即 13cb9609554dab

你可以用以下方式讓 Git 繪製出這兩個分支的所有提交:

$ git log --all --oneline --graph
* 70f727a (HEAD -> main, origin/main) d
* f654888 c
| * 13cb960 (origin/mybranch, mybranch) y
| * 9554dab x
|/
* 3997a46 b
* a74606f a

分支以提交 ID 的形式存儲

在 Git 的內部,分支會以一種微小的文本文件的形式存儲下來,其中包含了一個提交 ID。這就是我一開始提及到的「技術上正確」的定義。這個提交就是分支上最新的提交。

我們來看一下示例倉庫中 mainmybranch 的文本文件:

$ cat .git/refs/heads/main
70f727acbe9ea3e3ed3092605721d2eda8ebb3f4
$ cat .git/refs/heads/mybranch
13cb960ad86c78bfa2a85de21cd54818105692bc

這很好理解:70f727main 上的最新提交,而 13cb96mybranch 上的最新提交。

這樣做的原因是,每個提交都包含一種指向其父級的指針,所以 Git 可以通過追蹤這些指針鏈來找到分支上所有的提交。

正如我前文所述,這裡遺漏的一個重要因素是這兩個分支間的任何關聯關係。從這裡能看出,mybranchmain 的一個分支——這一點並沒有被表明出來。

既然我們已經探討了直觀理解的分支概念是如何不成立的,我接下來想討論的是,為何它在某些重要的方面又是如何成立的。

人們的直觀感覺通常並非全然錯誤

我發現,告訴人們他們對 Git 的直覺理解是「錯誤的」的說法頗為流行。我覺得這樣的說法有些可笑——總的來說,即使人們關於某個題目的直覺在某些方面在技術上不精確,但他們通常會有完全合理的理由來支持他們的直覺!即使是「不正確的」模型也可能極其有用。

現在,我們來討論三種情況,其中直覺上的「分支」概念與我們實際在操作中如何使用 Git 非常相符。

變基操作使用的是「直觀」的分支概念

現在,讓我們回到最初的圖片。

當你在 main 上對 mybranch 執行 變基 rebase 操作時,它將取出「直觀」分支上的提交(只有兩個紅色的提交)然後將它們應用到 main 上。

執行結果就是,只有兩次提交(xy)被複制。以下是相關操作的樣子:

$ git switch mybranch
$ git rebase main
$ git log --oneline mybranch
952fa64 (HEAD -> mybranch) y
7d50681 x
70f727a (origin/main, main) d
f654888 c
3997a46 b
a74606f a

在此,git rebase 創建了兩個新的提交(952fa647d50681),這兩個提交的信息來自之前的兩個 xy 提交。

所以直覺上的模型並不完全錯誤!它很精確地告訴你在變基中發生了什麼。

但因為 Git 不知道 mybranchmain 的一個分叉,你需要顯式地告訴它在何處進行變基。

合併操作也使用了「直觀」的分支概念

合併操作並不複製提交,但它們確實需要一個「 基礎 base 」提交:合併的工作原理是查看兩組更改(從共享基礎開始),然後將它們合併。

我們撤銷剛才完成的變基操作,然後看看合併基礎是什麼。

$ git switch mybranch
$ git reset --hard 13cb960  # 撤銷 rebase
$ git merge-base main mybranch
3997a466c50d2618f10d435d36ef12d5c6f62f57

這裡我們獲得了分支分離出來的「基礎」提交,也就是 3997a4。這正是你可能會基於我們的直觀圖片想到的提交。

GitHub 的拉取請求也使用了直觀的概念

如果我們在 GitHub 上創建一個拉取請求,打算將 mybranch 合併到 main,這個請求會展示出兩次提交:也就是 xy。這完全符合我們的預期,也和我們對分支的直觀認識相符。

我想,如果你在 GitLab 上發起一個合併請求,那顯示的內容應該會與此類似。

直觀理解頗為精準,但它有一定局限性

這使我們的對分支直觀定義看起來相當準確!這個「直觀」的概念和合併、變基操作以及 GitHub 拉取請求的工作方式完全吻合。

當你在進行合併、變基或創建拉取請求時,你需要明確指定另一個分支(如 git rebase main),因為 Git 不知道你的分支是基於哪個分支的。

然而,關於分支的直觀理解有一個比較嚴重的問題:你直覺上認為 main 分支和某個分離的分支有很大的區別,但 Git 並不清楚這點。

所以,現在我們要來討論一下 Git 分支的不同種類。

主幹和派生分支

對於人類來說,mainmybranch 有著顯著的區別,你可能針對如何使用它們,有著截然不同的意圖。

通常,我們會將某些分支視為「 主幹 trunk 」分支,同時將其他一些分支看作是「派生」。你甚至可能有派生的派生分支。

當然,Git 自身並沒有這樣的區分(「派生」是我剛剛構造的術語!),但是分支的種類確實會影響你如何處理它。

例如:

  • 你可能會想將 mybranch 變基到 main,但你大概不會想將 main 變基到 mybranch —— 那就太奇怪了!
  • 一般來說,人們在重寫「主幹」分支的歷史時比短期存在的派生分支更為謹慎。

Git 允許你進行「反向」的變基

我認為人們經常對 Git 感到困惑的一點是 —— 由於 Git 並沒有分支是否是另一個分支的「派生」的概念,它不會給你任何關於何時合適將分支 X 變基到分支 Y 的指引。這一切需要你自己去判斷。

例如,你可以執行以下命令:

$ git checkout main
$ git rebase mybranch

或者

$ git checkout mybranch
$ git rebase main

Git 將會欣然允許你進行任一操作,儘管在這個案例中 git rebase main 是極其正常的,而 git rebase mybranch 則顯得格外奇怪。許多人表示他們對此感到困惑,所以我提供了一個展示兩種變基類型的圖片以供參考:

相似地,你可以進行「反向」的合併,儘管這相較於反向變基要正常得多——將 mybranch 合併到 main 和將 main 合併到 mybranch 都有各自的益處。

下面是一個展示你可以進行的兩種合併方式的示意圖:

Git 對於分支之間缺乏層次結構感覺有些奇怪

我經常聽到 「main 分支沒什麼特別的」 的表述,而這令我感到困惑——對於我來說,我處理的大部分倉庫里,main 無疑是非常特別的!那麼人們為何會稱其為不特別呢?

我覺得,重點在於:儘管分支確實存在彼此間的關係(main 通常是非常特別的!),但 Git 並不知情這些關係。

每當你執行如 git rebasegit merge 這樣的 git 命令時,你都必須明確地告訴 Git 分支間的關係,如果你出錯,結果可能會相當混亂。

我不知道 Git 在此方面的設計究竟「對」還是「錯」(無疑它有利有弊,而我已對無休止的爭論感到厭倦),但我認為,這對於許多人來說,原因在於它有些出人意料。

Git 關於分支的用戶界面也同樣怪異

假設你只想查看某個分支上的「派生」提交,正如我們之前討論的,這是完全正常的需求。

下面是用 git log 查看我們分支上的兩次派生提交的方法:

$ git switch mybranch
$ git log main..mybranch --oneline
13cb960 (HEAD -> mybranch, origin/mybranch) y
9554dab x

你可以用 git diff 這樣查看同樣兩次提交的合併差異:

$ git diff main...mybranch

因此,如果你想使用 git log 查看 xy 這兩次提交,你需要用到兩個點(..),但查看同樣的提交使用 git diff,你卻需要用到三個點(...)。

我個人從來都記不住 ..... 的具體用意,所以我通常雖然它們在原則上可能很有用,但我選擇盡量避免使用它們。

在 GitHub 上,默認分支具有特殊性

同樣值得一提的是,在 GitHub 上存在一種「特殊的分支」:每一個 GitHub 倉庫都有一個「默認分支」(在 Git 術語中,就是 HEAD 所指向的地方),具有以下的特別之處:

  • 初次克隆倉庫時,默認會檢出這個分支
  • 它作為拉取請求的默認接收分支
  • GitHub 建議應該保護這個默認分支,防止被強制推送,等等。

很可能還有許多我未曾想到的場景。

總結

這些說法在回顧時看似是顯而易見的,但實際上我花費了大量時間去搞清楚一個更「直觀」的分支概念,這是因為我已經習慣了技術性的定義,「分支是對某次提交的引用」。

同樣,我也沒有真正去思索過如何在每次執行 git rebasegit merge 命令時,讓 Git 明確理解你分支之間的層次關係——對我而言,這已經成為第二天性,並沒有覺得有何困擾。但當我反思這個問題時,可以明顯看出,這很容易導致某些人混淆。

(題圖:MJ/a5a52832-fac8-4190-b3bd-fec70166aa16)

via: https://jvns.ca/blog/2023/11/23/branches-intuition-reality/

作者:Julia Evans 選題:lujun9972 譯者:ChatGPT 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國