深入理解 Git

深入理解 Git

Git 使我們?nèi)粘J褂玫拈_發(fā)工具,用于代碼的版本管理,但是我們常用的各種命令 git add, git commit, git push, git pull 等等究竟是啥樣子,帶著好奇心,趁著空重新讀了下 Git 的官方文檔。發(fā)現(xiàn)了一些除了剛剛提到的高級(jí)命令以外的低級(jí)命令,以及 Git 究竟是如何運(yùn)轉(zhuǎn)的。(當(dāng)然不是源碼解讀)

基本概念

git 對(duì)于文件的存儲(chǔ)位置進(jìn)行了3層分割,用于不同狀態(tài)下的文件。我們可以理解為3個(gè)箱子。

  • Work Directory
  • Index/Stage
  • Git Directory

Work Directory(俗稱工作區(qū))

工作區(qū)就是我們文件正常編輯的地方,我們?cè)谶@進(jìn)行代碼的編寫。當(dāng)我們完成之后,我們會(huì)發(fā)現(xiàn)我的的 cli 會(huì)提示我們這些文件被修改,從而方便我們區(qū)分哪些文件被修改了,哪些沒有被修改。

Index/Stage(暫存區(qū))

暫存區(qū)就是我們?cè)趫?zhí)行了 git add 之后,文件存放的地方,說明這些文件準(zhǔn)備進(jìn)行提交了。之所以有這么個(gè)地方,就是能夠?qū)⑿枰峤坏膬?nèi)容進(jìn)行一次性提交,從而避免了每次提交都存在歧義的情況。同時(shí)也方便我們對(duì)將要提交的地方進(jìn)行調(diào)整。

Git Directory

Git 的區(qū)域,用于將歷史記錄進(jìn)行統(tǒng)一的進(jìn)行管理。方便后續(xù)迭代的時(shí)候進(jìn)行一定的調(diào)整。git 中使用文件的形式進(jìn)行管理。

前面所說可能比較抽象,用一個(gè)簡單的話來說,當(dāng)我們大腦中有一個(gè)好的 idea 的時(shí)候,我們的大腦就是一個(gè) work directory,這個(gè)時(shí)候?yàn)榱吮苊馔?,或者它丟失,我們需要將他用筆寫下來,這個(gè)時(shí)候紙就是 Index/Stage。同時(shí)我們會(huì)進(jìn)行不斷的思考,那么就是不斷的修改 work directory 的內(nèi)容,并不斷 add 到 Index/Stage 當(dāng)中。當(dāng)這個(gè) idea 整理清楚的時(shí)候,我們需要將這張紙整理起來,就像是放到檔案室,這個(gè)時(shí)候檔案室就是我們的 Git Directory。

Git 基本對(duì)象

說完這些,就可以進(jìn)入我們的正題了。

Git 實(shí)際上有自己對(duì)于對(duì)象的定義。 Git 當(dāng)中存在 3種對(duì)象,blob,tree,commit。而所有對(duì)象都有自己的身份標(biāo)記 —— SHA-1碼。(對(duì)于 SHA-1 的解釋可以參見維基百科

blob

blob 就是我們所說的簡單文件,在谷歌中的翻譯 blob 指的是大的二進(jìn)制文件。在 git 中你可以認(rèn)為他就是文件對(duì)象。

tree

既然有了文件對(duì)象,那么需要對(duì)于文件對(duì)象進(jìn)行層級(jí)排列。這個(gè)時(shí)候就需要引入 tree 的概念了。Git 模仿了 Unix 的文件管理體系,不過他沒有系統(tǒng)的那么沉重,相對(duì)而言更加輕量級(jí),僅僅包含了里面有哪些文件。

commit

當(dāng)文件結(jié)構(gòu)已經(jīng)確定了,剩下的就是將這些內(nèi)容進(jìn)行提交了。每次當(dāng)我們將 tree 提交的時(shí)候,就創(chuàng)建了一個(gè) commit,而有 SHA-1 的 commit 便決定了 Git 的整個(gè)體系,能夠通過 SHA-1 進(jìn)行追蹤。

在清楚了上面的幾個(gè)關(guān)鍵點(diǎn)之后,就可以進(jìn)入我們的正題了。從我們創(chuàng)建 git 到提交完成整個(gè)工程,具體發(fā)生了些什么。

深入理解 Git

創(chuàng)建 git

當(dāng)我們完成了 git init 之后,我們創(chuàng)建了一個(gè)包含了 .git 的文件目錄

?  test git:(master) la
total 0
drwxr-xr-x  10 zkhcreator  staff   320B Jan 31 18:40 .git

這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn) .git 文件下的結(jié)構(gòu)如下:(附帶功能描述)

.git
├── HEAD    (用于管理 HEAD 所在的位置)
├── branches    (有哪些分支)
├── config  (配置文件)
├── description (對(duì)于這個(gè)工程的描述文件)
├── hooks   (本地的 git 的所有鉤子的配置)
│   ├── 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 (用于在 git 層面告知 git 哪些文件不需要進(jìn)行版本控制)
├── objects (所有 git 對(duì)象)
│   ├── info    (git 對(duì)象的信息,對(duì)象以及他的 SHA-1 存儲(chǔ)的地方)
│   └── pack    (git 執(zhí)行 gc 操作打包后的存儲(chǔ)地)
└── refs    (所有引用(references)的地方)
    ├── heads   (所有分支頭所在的位置)
    └── tags    (所有標(biāo)簽所在的位置)

上圖的文件樹結(jié)構(gòu)構(gòu)成了一個(gè)簡單的 git

創(chuàng)建 blob

按照往常的思路,我們需要?jiǎng)?chuàng)建文件并執(zhí)行 git add filename 但是這里我們不用這些高級(jí)命令,轉(zhuǎn)而使用低級(jí)命令。

首先我們創(chuàng)建一個(gè)文件 echo 'test1' | git hash-object -w --stdin 這樣就創(chuàng)建完成了一個(gè)文件,其中 -w 直接將數(shù)據(jù)寫入數(shù)據(jù)庫(準(zhǔn)確的說是文件當(dāng)中,因?yàn)?git 沒有數(shù)據(jù)庫這個(gè)概念,都是以文件進(jìn)行存儲(chǔ)的)當(dāng)中,--stdin 表示使用標(biāo)準(zhǔn)輸入輸出數(shù)據(jù)流格式進(jìn)行讀取。

此時(shí)我們會(huì)發(fā)現(xiàn)他返回了一條 SHA-1: a5bce3fd2565d8f458555a0c6f42d0504a848bd5,同時(shí)再次打印我們 .git 中的文件路徑,我們會(huì)發(fā)現(xiàn)他的 objects 路徑下多了 a5/bce3fd2565d8f458555a0c6f42d0504a848bd5 這樣的一個(gè)文件,這個(gè)和生成的 SHA-1 有一些微妙的聯(lián)系,實(shí)際上 git 取了 git 的前兩位作為一級(jí)目錄,并將后面的位數(shù)作為文件名。

如果我們嘗試去打開這個(gè)文件,我們會(huì)發(fā)現(xiàn)他是一堆亂碼,因?yàn)?git 已經(jīng)幫我轉(zhuǎn)我一次了,如果需要讀取里面的內(nèi)容,我們需要使用 git cat-file -p a5bce3fd2565d8f458555a0c6f42d0504a848bd5 去進(jìn)行展示,很明顯,他就會(huì)輸出我們剛剛通過數(shù)據(jù)流輸入輸出的 test1。上面命令中的 git cat-file -p 就是用來打印 SHA-1 對(duì)象的實(shí)際內(nèi)容。

有人可能會(huì)說,我們 git 不都是添加文件的么?你這樣直接寫數(shù)據(jù)庫算什么?

那我們來新建一個(gè)文件來重復(fù)以上操作。我們執(zhí)行以下命令

$ echo "version 1" > just_for_test.txt
$ git hash-object -w just_for_test.txt
83baae61804e65cc73a7201a7252750c76066a30

這個(gè)時(shí)候我們發(fā)現(xiàn)又多了一個(gè)文件 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30,然后我們?cè)賵?zhí)行一次寫入操作,嘗試去修改里面的內(nèi)容

echo "version 2" > just_for_test.txt
git hash-object -w just_for_test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn),結(jié)構(gòu)路徑除了我們剛剛生成的 83baae61(為了方便理解,取前幾位),還有一條新的 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a。很明顯,文件更新的情況,已經(jīng)被寫入到 git 的文件中。

但是我們執(zhí)行 git status 會(huì)發(fā)現(xiàn),文件還沒有提交,說明 git 在添加到 commit 前,會(huì)將文件進(jìn)行緩存。(不過什么時(shí)候進(jìn)行存儲(chǔ)數(shù)據(jù)庫的,可能需要看下源碼)

既然已經(jīng)有了 sha-1 并且我們已經(jīng)將文件存儲(chǔ)到數(shù)據(jù)庫,這個(gè)時(shí)候我們做數(shù)據(jù)恢復(fù)操作就很簡單了。只需要執(zhí)行 git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt 即可將前面的 version 1 恢復(fù)到文件目錄當(dāng)中。

這個(gè)時(shí)候如果我們重新執(zhí)行 git has-object -w just_for_test.txt 我們會(huì)發(fā)現(xiàn)目錄結(jié)構(gòu)沒有發(fā)生改變,因?yàn)?git 對(duì)于文件進(jìn)行存儲(chǔ)是基于文件內(nèi)部的內(nèi)容的,和其他的東西并無關(guān)系。

前面提到,git 當(dāng)中總共有3種類型,但是 SHA-1 是通的,所以,有時(shí)候我們需要確認(rèn)對(duì)應(yīng)的 SHA-1 是什么類型,我們就可以使用 git cat-file -t SHA-1 進(jìn)行打印,很明顯,這個(gè)文件是 blob 對(duì)象。

創(chuàng)建 tree

如果單純的文件,肯定是不能構(gòu)成樹目錄的,就像我們腦子中的點(diǎn)子,順序很亂,只有當(dāng)我們寫下來的時(shí)候,才能將它的順序理清楚。所以只有當(dāng)需要提交到 stash 才能確定誰再哪。

那么我們需要首先給文件提供一個(gè)暫存,故需要執(zhí)行 git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 just_for_test.txt 將在數(shù)據(jù)庫當(dāng)中指定的文件進(jìn)行存儲(chǔ)到暫存區(qū)。--add 標(biāo)識(shí)將工作區(qū)域的文件注冊(cè)到暫存區(qū)當(dāng)中,但是如果已經(jīng)添加,則不會(huì)重復(fù)添加。--cacheinfo 標(biāo)識(shí)需要添加的數(shù)據(jù)的詳細(xì)信息,比如說添加指定文件,以及他們的權(quán)限。其中的文件名是放到緩存中的文件名,一般我們通過 add 進(jìn)去的 work directory 和 stage 是一致的,但是可以手動(dòng)指定不同,SHA-1 用來確定文件的內(nèi)容具體是啥。此時(shí)我們執(zhí)行 git status 會(huì)發(fā)現(xiàn),文件已經(jīng)被添加到暫存區(qū)了。

然后通過 git write-tree 將當(dāng)前暫存區(qū)域的對(duì)象寫到樹對(duì)象當(dāng)中。這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn)它又給了我們一個(gè) SHA-1:ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 這個(gè)是生成的樹對(duì)象的標(biāo)識(shí)符,我們可以通過 git cat-file -t SHA-1 來獲得它的類型就是我們想要的 tree。 當(dāng)然通過搜索 $ find .git/objects/ff 也能在 objects 中很容易找到我們需要的文件。

那么樹對(duì)象能進(jìn)行嵌套么?答案當(dāng)然是肯定的。

我們可以通過 git read-tree --prefix=bak ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 來將原有的 tree 進(jìn)行備份到當(dāng)前的 tree 下面,由于老的 tree 是 object 對(duì)象,所以當(dāng)你讀取出來之后,你在 git status 下你會(huì)發(fā)現(xiàn)他被寫入暫存區(qū)了,但是在 work directory 當(dāng)中是需要?jiǎng)h除的,也就是這個(gè)文件本身是不存在的,這都是因?yàn)闊o中生有,或者說強(qiáng)制讀取對(duì)象的造成的。

創(chuàng)建 commit

既然完成了 stage 的寫入,最后一步就是需要將當(dāng)前的 tree 進(jìn)行提交,并創(chuàng)建提交對(duì)象。這時(shí)候只需要執(zhí)行 echo "first commit" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa 這個(gè)時(shí)候,我們通過 git log 會(huì)發(fā)現(xiàn),我們已經(jīng)提交了 commit。但是 git status 還是有文件沒有更新。這個(gè)原因也很簡單,就是因?yàn)槲覀儧]有創(chuàng)建分支,這就導(dǎo)致 commit 不知道該提交到哪里。 只需要 git branch new_branch_name $(echo "commit message" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa) 即可。

最后

當(dāng)我們提交完上面的操作之后,也就是完成了我們最基本的常用命令的底層的操作。這個(gè)時(shí)候我們重新打印下我們的 .git 文件。會(huì)發(fā)現(xiàn)里面多了好多東西,除了我們自己的 objects 還有很多剛剛注釋里面提到的,但是文章當(dāng)中沒有提到的內(nèi)容。 比如說:

  • COMMIT_EDITMSG:最近一次 commit 的內(nèi)容
  • HEAD:當(dāng)前的頭的位置,ref: refs/heads/master 即引用的位置為 refs/heads/master 文件夾中的內(nèi)容。
  • INDEX:放置過 index 之后,即執(zhí)行 git update-index 之后 index 的內(nèi)容,編碼過后的文件,不能容易的看懂。
  • logs/HEAD:HEAD 切換的日志文件。
  • logs/refs/heads/master: master 的 git 修改日志情況。左邊為 0,右邊有值壽命這個(gè)是新添加的。相反右邊為 0說明文件是移除的。同時(shí)數(shù)字前4位標(biāo)識(shí)所有內(nèi)容長度的十六進(jìn)制數(shù),主要方便上傳下載的智能協(xié)議的同步操作。具體內(nèi)容可見 Git 傳輸協(xié)議
0000000000000000000000000000000000000000 d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f zkhCreator <zkhCreator@gmail.com> 1548937962 +0800    commit (initial): test
d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f b6542b871b52bdf66481d03127620e1d7b7d37c9 zkhCreator <zkhCreator@gmail.com> 1548938032 +0800    commit: test2
  • refs/heads/master:當(dāng)前這個(gè) master 對(duì)應(yīng)的提交對(duì)象的 SHA-1 值。
  • refs/tags:對(duì)應(yīng) tag 的指向的提交對(duì)象的 SHA-1 值存儲(chǔ)的地方。

最后的最后

當(dāng)然其中還存在 objects/infoobjects/pack 這兩個(gè)。你可以嘗試在工程中執(zhí)行 git gc 來對(duì)當(dāng)前內(nèi)容進(jìn)行整理。你會(huì)發(fā)現(xiàn)文件當(dāng)中的 objects/ 目錄下干凈了很多,僅僅留下了 objects/info/packsobjects/pack 。這個(gè)時(shí)候?qū)嶋H上 git 將你的文件進(jìn)行了打包操作,從而減少了整個(gè)工程的體積。路徑中的 pack-SHA1.idx 就是這個(gè)包的索引,里面包含了打包之后的文件信息(主要用于上傳下載的過程中的不常用的文件索引,可以通過 git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx 進(jìn)行查看文件目錄),pack-SHA1.pack 則是壓縮的 git 對(duì)象的集合。對(duì)于細(xì)節(jié)想了解的同學(xué)可以查看官網(wǎng)的Git 內(nèi)部文件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容