Git內部原理剖析,有比這還詳細的嗎?

1.1. 為什麼寫這篇文章

寫這篇文章的本意有二:

  1. 工作安排原因,常有同事詢問我一些關於 Git 的問題,總覺得自己解釋的不夠透徹,因此覺得有必要深入瞭解一下。
  2. 目前中文的 Git 教程往往本末倒置, 一味從版本管理工具的角度去堆砌命令 ,而沒有把握住Git的本質,導致讀者知道的命令愈多,愈覺得 Git 複雜不友好。

本文中,筆者會通過實例演示+原理解釋的方式進行剖析,並提出一些平時我們不易察覺的問題。

1.2. Git產生的背景

Git 誕生於2005年,當時 Linux 內核開發者可以免費使用 BitKeeper 作為源碼管理工具,但是其作者認為部分開發者對 BitKeeper 進行逆向工程有悖原則,因而收回了使用權限,在這種危機時刻, Linus 再一次將個人英雄主義發揮到了極致,以主導設計開發了 Git 。根據這段背景我們需要意識到這麼幾個問題:

  1. Git 最早用來解決 Linux 內核的開發,因而其功能特點都是面向這種 參與者龐大且分散的協作開發模式設計的,企業的小團隊可能體會不到 Git 的真正強大之處 。
  2. Git 的設計者和最早的使用者都是 Linux 內核開發者,比起 GUI 界面, 他們更喜歡命令行,更認同 Unix 的設計哲學 ,因而對於習慣了 GUI 界面的開發者(前端、移動端等)Git會顯得十分“笨拙”。

理解這些背景對於我們認識 Git 十分重要,舉一個例子,我們希望看到所有代碼分支的最後提交時間和提交者,這種功能是高度定製的,如果 GUI 工具沒有提供,那我們便無能為力,但是 Git 可以,通過 靈活的參數和 Linux 強大的工具集( grep 、 awk ) ,我們可以自動封裝出這個命令並使用:

<code>gs_branch_last_commit() {    git fetch --prune    git for-each-ref --sort='-committerdate' --format="%(refname:short) %09 %(authorname) %09 %(committerdate:relative)"  \\        | grep  --line-buffered "origin" \\        | awk '{printf "%-50s%-25s%s %s %s\\n",$1,$2,$3,$4,$5}'}/<code>

最後需要強調的是, Git本意不是做一個版本管理工具,而是文件管理系統(Git is a content-addressable filesystem) ,正如Linux在早期的郵件中所述:

In many ways you can just see git as a filesystem - it’s content- addressable, and it has a notion of versioning, but I really really designed it coming at the problem from the viewpoint of a filesystemperson (hey, kernels is what I do), and I actually have absolutely zero interest in creating a traditional SCM system.

—Linus Torvalds

在讀完本文後,相信讀者能更深刻地理解這段話。

SCM(即 Software configuration management)是一種更廣義的版本管理, Linus 更願意直接將其解釋為 Source Code Management。


1.3. SCM的三個問題

The Architecture of Open Source Applications (Volume 2) 中提到任何一個SCM軟件都需要解決三個問題, 以保證軟件在開發過程中任一時間的內容都可以被追溯,並使得不同開發者可以協作開發 。這三個問題是:

  1. 存儲內容(Storing content)
  2. 追蹤內容的變更(Tracking changes to the content (history including merge metadata))
  3. 向其他開發者分發內容及其變更(Distributing the content and history with collaborators)

Git也不例外,接下來本文將圍繞這三個問題,並結合Git自身的一些特點,進行剖析。

2. 術語

由於讀者可能對於Git的內部原理不甚熟悉,所以這裡把專業詞彙先列出來:

  • .git Directory & Working Directory : .git 目錄是 Git 存儲信息和操作信息的目錄, Working Directory 是我們實際操作的目錄。
  • Git Object : Git 對象,我們的文件、目錄和提交記錄都會以 Git Object 的格式存儲在 .git 目錄中。
  • Git Reference : Git 引用,我們的分支、遠程分支、tag的索引都是已 Git Reference 的形式存儲,本質是一個包含 SHA1 值的40個字符的16進制字符串。
  • SHA1 : 所有的文件的內容都會通過該算法計算出其(其實還有一個header) SHA1值作為Git 對象的文件名(其實就是數據庫中的Key)。
  • plumbing & porcelain : Git的命令分類方式,前者是晦澀的底層命令,直接操作文件,後者是面向版本管理的高級命令。

3. Git 對象

3.1. .git目錄

當我們在某個目錄下執行 git init 命令時,該目錄就會成為 Git 的一個工作目錄(Working Directory),而該目錄下面的 .git 目錄則是 工作目錄的全部歷史在 Git 內的表示 。

在開始Git內部原理的探索之旅前,我們有必要認識一下 .git 目錄,我們在命令行或者 GUI 界面的各種操作,本質都是操作 .git 目錄下的文件。現在我們通過 git init 創建一個倉庫,並通過 tree 命令查看 .git 目錄的結構:

<code>$ tree .git.git├── HEAD├── branches├── config├── description├── hooks│ ├── applypatch-msg.sample│ ├── commit-msg.sample│ ├── fsmonitor-watchman.sample│ ├── post-update.sample│ ├── pre-applypatch.sample│ ├── pre-commit.sample│ ├── pre-push.sample│ ├── pre-rebase.sample│ ├── pre-receive.sample│ ├── prepare-commit-msg.sample│ └── update.sample├── info│ └── exclude├── objects│ ├── info│ └── pack└── refs    ├── heads    └── tags9 directories, 15 files/<code>

各個目錄/文件的作用如下

文件

<code>HEADconfigdescriptionindex/<code>

目錄

<code>hooksinfo/excludeobjectsrefs/<code>

其中 objects refs 將是本文剖析的重點,也是Git的核心。

3.2. plumbing 和 porcelain

在Git中,存在兩種命令: plumbing 和 porcelain ,前者將Git作為一個文件管理系統,所有的命令都十分的底層、抽象,例如上文使用的 git for-each-ref 就是一個 plumbing 命令;對應的,後者將Git作為一個版本管理系統,所有的命令都更加抽象和直白,例如我們常見的 git add / git commit / git push 三兄弟。本文將基於常用的 porcelain 命令進行剖析,以揭示其背後的真實操作,並在某些時刻使用一些 plumbing 命令,以幫助讀者從文件的角度進行更深刻的理解。

3.3. Git對象類型

前面說過,Git的本質是一個 content-addressable filesystem (基於內容尋址的文件系統)

既然是一個文件管理系統,那麼文件有哪些類型呢?一般來說有 目錄和文件 。對於 Git 來說,還需要記錄變更,這也可以認為是一種特殊的文件類型。最後,Git還提供了一種tag類型的文件,它為某次提交提供了一個永久索引,因為對於一個SCM系統來說,記錄某次重大改動(如版本發佈)是十分有必要的。

在Git中,可以用一個有向無環圖來表示Git對象的組織方式:


Git內部原理剖析,有比這還詳細的嗎?

所以Git對象一共有四種類型:

<code>blobtreecommittag/<code>

3.4. Git對象操作

那麼這四種文件類型具體長什麼樣呢?這就不得不提Git的對象模型了,在Git中,所有的對象都會通過 zlib 壓縮成一個文件名為其 SHA1 值的文件,為了更具體解釋這句話,我們開始實驗。首先為了方便後面的表示,我設計了一個命令,藉助了兩個 plumbing 命令: rev-list cat-file,用來打印所有Git對象的內容:

<code>print_all_object() {    for object in `git rev-list --objects --all | cut -d ' ' -f 1`; do        echo 'SHA1: '$object        git cat-file -p $object        echo '^'    done}/<code>

第一步,初始化一個倉庫並添加文件(為了模擬真實場景,這裡用了多級目錄):

<code>echo "first file" > first.txtmkdir -p second/thirdecho "second file" > second/second.txtecho "third file" > second/third/third.txtgit add first.txtgit add secondgit commit -m "first commit"echo '~~~~'treeprint_all_object/<code>

此時工作目錄如下:

<code>.├── first.txt└── second    ├── second.txt    └── third        └── third.txt2 directories, 3 files/<code>

Git對象的內容如下:

<code>SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9 #(1)tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao> 1575091820 +0800committer vimerzhao <vimerzhao> 1575091820 +0800first commit^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8 #(2)100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9a #(3)first file^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450 #(4)100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8b #(5)second file^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc #(6)100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359 #(7)third file^/<vimerzhao>/<vimerzhao>/<code>
  1. commit 類型的 Git 對象,記錄了 commit 指向的 tree 對象,以及提交者(author/committer)的信息
  2. tree 類型的 Git 對象,該對象包含一個名為 first.txt 的文件和一個名為 second 的目錄
  3. blob 類型的 Git 對象,該對象的內容為 first file
  4. 同2
  5. 同3
  6. 同2
  7. 同3

可以看出, Git 的 commit 對象可以通過 SHA1 找到 tree 對象,tree 對象可以通過 SHA1 找到其他 tree 和 blob ,在Git中, SHA1就是Git對象的指針 。此外,Git 使用這種文本化的表示方式也符合Unix 一切皆文本的哲學。為了便於理解,上面的信息可以按照上文的模型畫出對應的有向無環圖(暫時省略了Git引用):

Git內部原理剖析,有比這還詳細的嗎?


接下來我們修改一個文件,再次提交

<code>echo "first file modified" > first.txtgit add first.txtgit commit -m "second commit"echo '~~~~'print_all_object/<code>

內容如下:

<code>SHA1: 67f9d83a9ef370c057accf103e6502d3c8a56048tree 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3parent b8a7759d225d7ca4952c57c9ba785a6692a075a9author vimerzhao <vimerzhao> 1575091920 +0800committer vimerzhao <vimerzhao> 1575091920 +0800second commit^SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao> 1575091820 +0800committer vimerzhao <vimerzhao> 1575091820 +0800first commit^SHA1: 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336first file modified^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8bsecond file^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359third file^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9afirst file^/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<code>

注意commit增加了一個parent字段,此時圖變成了:


Git內部原理剖析,有比這還詳細的嗎?


接下來我們再刪除一個文件,再次提交

<code>rm second/second.txtgit add secondgit commit -m "third commit"git tag -a v3 -m "tag third commit"echo '~~~~'print_all_object/<code>

內容如下:

<code>SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao> 1575091990 +0800committer vimerzhao <vimerzhao> 1575091990 +0800third commit^SHA1: 67f9d83a9ef370c057accf103e6502d3c8a56048tree 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3parent b8a7759d225d7ca4952c57c9ba785a6692a075a9author vimerzhao <vimerzhao> 1575091920 +0800committer vimerzhao <vimerzhao> 1575091920 +0800second commit^SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao> 1575091820 +0800committer vimerzhao <vimerzhao> 1575091820 +0800first commit^SHA1: 825133937ceb744ad49a71883f70237a4a1dfc1dobject 78913509ce663ff1e58e47043b015283856779dctype committag v3tagger vimerzhao <vimerzhao> 1575091990 +0800tag third commit^SHA1: 6ac7a28caaf73725fc3383b916447e839e3c2d50100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree d4046dced1e51bbc931b845cfea1c529fec7256c second^SHA1: 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336first file modified^SHA1: d4046dced1e51bbc931b845cfea1c529fec7256c040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359third file^SHA1: 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8bsecond file^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9afirst file^/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<code>

注意多了一個tag類型的Git對象,指向第三次提交,此時圖變成了:


Git內部原理剖析,有比這還詳細的嗎?

以上,我們演示了Git在進行常見的增刪改的時候,背後發生的事情。

4. Git 引用

Git 創建分支的成本及其低廉。

Creating a branch is nothing more than just writing 40 characters to a file.

—Linus Torvalds

4.1. 分支

對於上面的倉庫,我們此時看一下 HEAD 文件和 refs 目錄下的內容,然後創建兩個新的分支並提交一些內容:

<code>cat .git/HEADcat .git/refs/heads/mastergit branch feature1git checkout -b feature2 # 創建兩個新的分支echo "new feature2" > feature.txtgit add feature.txt && git commit -m "add feature"git checkout mastercat .git/refs/heads/master # 查看每個分支指向的提交cat .git/refs/heads/feature1cat .git/refs/heads/feature2/<code>

得到輸出

<code>78913509ce663ff1e58e47043b015283856779dc78913509ce663ff1e58e47043b015283856779dca5596a09a7e83f83bf713e81e7653fa652906090/<code>

(為了節省篇幅,後面不會貼出 print_all_object 的全部執行結果)

由此可以更新一下我們的有向無環圖(為了突出重點,本章節不畫出 Git 對象):


Git內部原理剖析,有比這還詳細的嗎?


4.2. 合併

現在master分支做一次commit,執行一次merge

<code>echo "anothre feature" > feature.txtgit add feature.txt && git commit -m "add another feature"git merge feature2/<code>

解衝突,然後提交

<code>vim feature.txtgit add feature.txt && git commit -m "handle conflict"cat feature.txtprint_all_object/<code>

部分結果如下:

<code>anothre featurenew feature2SHA1: 9c6eb61ba181a070e06d8c5767ea3bdca5f40558tree da90387f4018255a3a37ff2933a8810cf3eccc1fparent 8a1105568902f643a60165f3aa8067e8f16b9ce0parent a5596a09a7e83f83bf713e81e7653fa652906090author vimerzhao <vimerzhao> 1575092878 +0800committer vimerzhao <vimerzhao> 1575092946 +0800handle conflict^SHA1: 8a1105568902f643a60165f3aa8067e8f16b9ce0tree c1ba2f6bb8f4412460347c204714a580155a1180parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao> 1575092846 +0800committer vimerzhao <vimerzhao> 1575092846 +0800add another feature^SHA1: a5596a09a7e83f83bf713e81e7653fa652906090tree 715230bf7c2800c4e745151ce19859127ebbb4b0parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao> 1575092660 +0800committer vimerzhao <vimerzhao> 1575092660 +0800add feature^SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao> 1575091990 +0800committer vimerzhao <vimerzhao> 1575091990 +0800third commit^/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<code>

注意,最後一次提交有兩個 parent commit 。此時,有向無環圖變為:


Git內部原理剖析,有比這還詳細的嗎?

4.3. 變基

現在在分支feature1做一次提交,然後使用rebase的方式同步主幹:

<code>git checkout -b feature1echo "add feature1" > feature1.txtgit add feature1.txt && git commit -m "add feature1"git rebase mastercat .git/refs/heads/mastercat .git/refs/heads/feature1cat .git/refs/heads/feature2treeprint_all_object/<code>

部分輸出為:

<code>9c6eb61ba181a070e06d8c5767ea3bdca5f40558ce72867669c6f5ece8f2aef71476a458e502485da5596a09a7e83f83bf713e81e7653fa652906090.├── feature.txt├── feature1.txt├── first.txt└── second    └── third        └── third.txt2 directories, 4 filesSHA1: ce72867669c6f5ece8f2aef71476a458e502485dtree 9a004023f848b224cc79c7aa069738d24dfc8c27parent 9c6eb61ba181a070e06d8c5767ea3bdca5f40558author vimerzhao <vimerzhao> 1575093789 +0800committer vimerzhao <vimerzhao> 1575093789 +0800add feature1^SHA1: 9c6eb61ba181a070e06d8c5767ea3bdca5f40558tree da90387f4018255a3a37ff2933a8810cf3eccc1fparent 8a1105568902f643a60165f3aa8067e8f16b9ce0parent a5596a09a7e83f83bf713e81e7653fa652906090author vimerzhao <vimerzhao> 1575092878 +0800committer vimerzhao <vimerzhao> 1575092946 +0800handle conflict^SHA1: 8a1105568902f643a60165f3aa8067e8f16b9ce0tree c1ba2f6bb8f4412460347c204714a580155a1180parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao> 1575092846 +0800committer vimerzhao <vimerzhao> 1575092846 +0800add another feature^SHA1: a5596a09a7e83f83bf713e81e7653fa652906090tree 715230bf7c2800c4e745151ce19859127ebbb4b0parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao> 1575092660 +0800committer vimerzhao <vimerzhao> 1575092660 +0800add feature^SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao> 1575091990 +0800committer vimerzhao <vimerzhao> 1575091990 +0800third commit^/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<vimerzhao>/<code>

需要注意的是,由於執行了rebase操作,commit ce7286 的 parent commit 並不是發生提交時的 commit( 789135 )了,而是master分支的最新commit( 9c6eb6 )此時有向無環圖變為:


Git內部原理剖析,有比這還詳細的嗎?

覺得此文不錯的大佬們可以多多關注或者幫忙轉發分享一下哦,感謝!!!!

Git內部原理剖析,有比這還詳細的嗎?


分享到:


相關文章: