git 是一種程序員幾乎天天都會用到的工具,給咱們代碼管理帶去了極大的方便。以往的 git 介紹,可能是介紹git 的高級命令,如git rebse
、git cherry-picker
、git bisect
等,少有看到剖析git 內部原理的。緣由也很簡單,即便對 git 的原理不甚瞭解,也並不會影響咱們熟練使用 git。可是不少事咱們不光要知其然,更要知其因此然,方能觸類旁通。php
在介紹 git 的原理以前,先介紹幾個基本概念和會用到的命令,以便你們在閱讀文章時能更加輕鬆地理解。git
git add
,對應工做區(Working Directory)。git add
後所處的狀態, 對應暫存區(Stage)。git commite
後所處的狀態,對應版本庫(Commit History)。git cat-file
:查看 git 對象的瑞士軍刀,能轉譯二進制文件爲可讀文件。git hash-object -w filename
:查看到 git 已存儲的數據hexdump -C filename
:查看二進制文件的十六進制編碼當你在一個新目錄或已有目錄內執行 git init
時,git 會建立一個 .git 目錄,幾乎全部 git 存儲和操做的內容都位於該目錄下。記得在剛接觸 git 時,由於對 stage
、branch
等概念不夠了解,還作出過備份整個項目的蠢事。其實若是真要作備份,備份當前的 .git 文件便可。程序員
下圖爲剛初始化的 .git 的目錄結構(不一樣版本的 git 會有所不一樣),幾乎全部的 git 操做和存儲都位於該目錄下。本次真要介紹四個核心文件或目錄:HEAD 及 index 文件,objects 及 refs 目錄。算法
簡而言之,git 從核心上來看不過是簡單地存儲鍵值對(key-value)。它容許插入任意類型的內容,並會返回一個鍵值,經過該鍵值能夠在任什麼時候候再取出該內容。shell
先拋出一個問題:一個空的文件夾是否能添加到 git 項目中?不少人可能都知道答案,可是爲何吶?經過對 git add
背後原理的解析,相信你能夠得出一個確切的答案。bash
$ mkdir alpha && cd alpha
$ git init
$ mkdir data
複製代碼
經過執行上面的命令,咱們建立了一個名爲 alpha
的文件夾,而且將其初始化爲一個 git 項目,再新建一個 data
空文件夾。經過執行 git add data
命令但願把 data
空文件夾添加到暫存區中,發現執行後並無發生任何變化,執行 git status
查看倉庫的狀態,拋出這樣一段提示:ide
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
複製代碼
若是在 data
文件夾內再新建一個空文件夾又會如何吶,發現仍是沒有任何變化。這樣就得出了開頭那個問題的答案,空的文件夾是沒法添加到 git 項目中的。再查看一下 .git
目錄,也沒有發生任何變化。工具
$ echo 'a' > data/letter.txt
$ git status
Untracked files:
(use "git add <file>..." to include in what will be committed)
data/
nothing added to commit but untracked files present (use "git add" to track)
複製代碼
在 data
目錄下新建一個名爲 letter.text
的文件,寫入 a
。發現倉庫的狀態發生了變化,可是 .git
目錄並無發生變化,說明在工做區的操做並不會產生 git 歷史記錄。 執行 git add data
命令,發現 .git
目錄終於發生變化了。 分別多了 index
文件和 objects/78/...
文件。ui
以前說過,git 的存儲實際上是以鍵值對的形式存在的。index
是一個列表,每一行維護一個文件名到 BLOB 哈希值的映射。objects
目錄下存放的就是 value,這個BLOB文件包含了data/letter.txt
文件壓縮後的內容,並之內容的哈希值做爲文件名。哈希是一段算法,它將給定內容轉換爲更小的,且能惟一肯定原內容的值。 我曾經嘗試直接打開這些文件,可是打開都是一串亂碼。能夠經過十六進制編碼的方式打開這些文件,可是畢竟不是機器,想要看明白仍是有一些難度的🤣。 編碼
幸虧 git 提供了一把瑞士軍刀(git cat-file
)給咱們使用,能夠將數據內容取回。 打印出 data/letter.txt
中的內容爲 a
。
$ git cat-file -p 78981922613b2afb6025042ff6bd878ac1994e85
a
複製代碼
至於 git/index
中的內容,能夠經過 git ls-file
查看。
$ git ls-file --stage
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 data/letter.txt
複製代碼
正如以前所說的,index
文件存放的是一個列表,記錄了哈希值對應的文件。7898
這個 BLOB 文件存儲的是 data/letter.txt
中的內容。objects
目錄下的結點是不可變的。這意味着它的內容能夠編輯,但不能刪除。添加的文件內容和建立的提交都保存在 objects
目錄下。(注:git prune
刪除全部不能被ref訪問到的對象,執行此命令可能會丟失數據。)
$ git commit -m 'a1'
[master (root-commit) b83b660] a1
1 file changed, 1 insertion(+)
create mode 100644 data/letter.txt
$ git status
On branch master
nothing to commit, working tree clean
複製代碼
執行 git commit
命令後,文件被保存到了版本庫中,倉庫處於 clean
的狀態。再看看 .git
目錄,發現多了 logs
目錄和 objects
下的三個文件。
提交命令其實包含了三個步驟:
data/letter.txt
文件的全部信息,咱們可使用這些信息來恢復 data/letter.txt
文件。空格分隔的第一部分表示該文件的權限,代表是一個普通文件;第二部分表示該記錄對應的是一個 BLOB 對象,第三部分是該BLOB 的哈希值,第四部分記錄了文件名。$ git cat-file -p e908cfc6e086c91a073c55a6882adebfc9c4520c
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 letter.txt
複製代碼
用圖示表式即爲:data
目錄對應的 tree 對象指向對應data/letter.txt
那麼問題來了,data 並不是根目錄,文件的指向確定是從根目錄開始。
$ git cat-file -p master^{tree}
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
$ git cat-file -p 9745002f161a1be75bf65f869ab16743da2a6fda
040000 tree e908cfc6e086c91a073c55a6882adebfc9c4520c data
複製代碼
master^{tree}
表示 master (當前)分支上最新提交指向的 tree 對象。從給出的結果中能夠看到根目錄(root)指向了 data
目錄,圖示以下:
commit message
等信息,這些信息天然也存放在objects
目錄下:$ git cat-file -p b83b66096fb5b5225ec0c5741f45d334ceb94046
tree 9745002f161a1be75bf65f869ab16743da2a6fda
author Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
committer Bill Qiu <fanglaiq@gmail.com> 1536220281 +0800
a1
複製代碼
第一行指向一個tree對象。經過這裏的哈希值,咱們能夠找到對應工做區根目錄(即 alpha 目錄)的 tree 對象,最後一行是提交信息。
HEAD
文件中:ref: refs/heads/master
。HEAD 指向 master,master 就是咱們當前分支。由於是第一個提交,表明master引用的文件還不存在。git 會自動建立 .git/refs/heads/master
,並寫入提交對象的哈希值:b83b66096fb5b5225ec0c5741f45d334ceb94046
。最後,全部 BLOB 文件所存儲的信息都經過 tree 對象串聯了起來,就比如 key-value
的形式。
再來回答下開頭提出的問題吧,一個空的文件夾是否能添加到 git 項目中? 答:不能夠。由於 git 使用的索引機制,是以文件爲最小單位存儲內容,跟蹤變化的。 那麼怎麼作纔可使這個文件夾存在吶?一般的作法是在裏面新建一個名爲 .gitkeep
的文件。