Git 使咱們天天使用得最多的工具之一,它是 linux 內核的最先做者 Linus Torvalds 建立的全新版本控制工具,和 linux 同樣的簡單好用,這篇文章就簡單地講一下 git 是如何工做的linux
https://git-scm.com/下載安裝便可git
我在個人桌面上建立了一個名爲 git_demo 的文件夾github
我這裏是直接拆分窗口,便於顯示,並導航到一樣的目錄算法
在右側窗口中,輸入如下命令,用來監控目錄變化緩存
watch -d -n 1 tree --charset=ascii -aF
該命令綜合了兩個工具,一個是watch
一個是tree
,經過 watch 來定時(上面指定每隔 1 秒)執行一次後面的命令,咱們就可以看到實時的目錄變化了bash
補上一個動圖版本編輯器
在左側命令行中執行熟悉的git init
,咱們將在當前目錄建立一個本地的 git 倉庫,執行完後是這樣的工具
在右側咱們能夠看到,本地多出了一個名爲.git
的隱藏目錄,裏面有一些文件夾,咱們先看第一級測試
`-- .git/ |-- HEAD |-- config |-- description |-- hooks/ |-- info/ |-- objects/ `-- refs/
其中有這麼幾個文件/夾this
HEAD 他的內容是
ref: refs/heads/master
說明如今的引用指向的是refs/heads/master
這個目錄,其實也是咱們當前工做的位置
config 其實就是本地的 git config
[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true
是一種類toml的數據格式
description 就是一個描述,平時用不到
Unnamed repository; edit this file 'description' to name the repository.
他的做用就是在你執行一些特定 git 命令(好比 git commit)的時候,順帶執行你指定的 bash 命令,很強大的功能。好比咱們想在提交前執行代碼測試,咱們就能夠去編輯 hooks 裏面的pre-commit.sample
這個文件,而且去掉文件名末尾的.sample
,那在你下次執行git commit
的時候,就會先運行這個 bash,只有當整個 bash 執行成功後(退出碼爲 0),纔會執行你剛纔的git commit
.
默認的已經存放了一個名爲exclude
的文件,其實就是一個.gitignore
文件
這個文件夾內,最重要的就是HEAD
文件,objects
文件夾,refs
文件夾,git 經過先訪問HEAD
文件,找到咱們當前工做的分支,而後再去refs
文件夾中找到對應的分支描述,這個描述文件正是指向了objects
文件夾內的某一個文件,再經過這個文件,咱們就能獲得當前分支當前版本的全部源碼了!
上面看了,HEAD
文件其實就只有一行,指向了refs
目錄的一個文件
咱們就來探祕一下refs
文件夾和objects
文件夾究竟是在作什麼
爲了不過多的文件影響,咱們先將 hooks 文件夾裏的文件都刪掉,只保留 pre-commit.sample
如今咱們在git_demo
目錄下新建一個文件名爲foo.txt
,而且在裏面輸入如下數據並保存(注意下面的文本結尾沒有空格,沒有換行)
this is foo
再查看咱們的目錄結構
|-- .git/ | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- info/ | | `-- exclude | |-- objects/ | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | `-- tags/ `-- foo.txt
如期地多出了一個foo.txt
文件
其實就是咱們無比熟悉的git add
命令了,咱們在左側命令行執行git add foo.txt
,能夠看到目錄樹是這樣
|-- .git/ | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- objects/ | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | `-- tags/ `-- foo.txt
在objects
文件夾中,多出了一個名爲9b
的文件夾,而且裏面有一個名爲3a97dafadb12faf10cf1a1f3a32f63eaa7220a
的文件,咱們看看這個文件是什麼呢
看起來是個二進制文件,其實這個就是咱們的源碼了😂,只是被壓縮處理了
git 對每次添加進緩存區的文件,都會執行一次 defete 壓縮而後存儲
咱們執行一下這個命令,這個命令的功能是將blob 12\0this is foo
這個字符串,用 openssl 中的 sha1 算法計算摘要
echo "blob 12\0this is foo" | openssl sha1
發現了什麼,生成的摘要結果(9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a)和咱們目錄(9b)以及文件名(3a97dafadb12faf10cf1a1f3a32f63eaa7220a)加起來的字符串同樣
git 會將文件以blob 文件長度(單位B)\0文件內容
進行計算 sha1,將獲得的結果,前兩位做爲文件夾名稱,剩下的部分做爲文件名,將咱們的文件存儲在objects
目錄下
好了,咱們基本知道了,git 是如何存儲咱們的文件了,那咱們提交一次代碼,看會有什麼變化
咱們也可使用 git 內置的一個查看命令,來查看對應的提交文件
# 查看 object 的類型 > git cat-file -t 9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a blob # 查看 object 的類型,後面的提交 hash 其實能夠簡寫前面一部分,只要保證只能檢索出一個文件便可 > git cat-file -t 9b3a blob # 查看 object 的內容 > git cat-file -p 9b3a this is foo
很常規,咱們就執行git commit -m "add foo.txt"
這個命令進行提交就行了,結果以下
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
文件夾瞬間多了幾個文件,objects 目錄中多出了兩個文件夾82
、f8
以及相應的文件,refs
文件夾中則是多出了一個名爲master
的文件,他就是指向咱們當前的分支啦
查看一下refs/headers/master
文件裏面內容是什麼
> cat .git/refs/heads/master 82be13e5bf9fafe4db5ad38c76a5c0116e156953
是一個 hash,根據這個hash,咱們可以在objects
中找到對應的文件了,使用git cat-file
查看一下
> git cat-file -t 82be13e5bf9fafe4db5ad38c76a5c0116e156953 commit > git cat-file -p 82be13e5bf9fafe4db5ad38c76a5c0116e156953 tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 author Aiello <aiello.chan@gmail.com> 1577369635 +0800 committer Aiello <aiello.chan@gmail.com> 1577369635 +0800 add foo.txt
說明這個文件是一個commit
類型的文件,文件內容則是指向了一個類型爲tree
的文件,咱們繼續順藤摸瓜,去找這個tree
類型的文件包含了什麼信息
> git cat-file -t f8aa85021bfe8c10d9517e22feda4fc67d0a4095 tree > git cat-file -p f8aa85021bfe8c10d9517e22feda4fc67d0a4095 100644 blob 9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a foo.txt
能夠看到,這個tree
類型的文件,指向了咱們的源文件,咱們經過以前相同的方法進行一次 hash 計算試試
# 獲取提交的類型 > git cat-file -t f8aa85021bfe8c10d9517e22feda4fc67d0a4095 tree # 獲取文件內容 > git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 100644 foo.txt�:������ ��/c� # 計算內容的字節數 > git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 | wc -c 35 # 合起來計算 # 按照 類型 字節數\0內容 的公式計算 > (printf "tree %s\0" $(git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 | wc -c); git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095) | openssl sha1 f8aa85021bfe8c10d9517e22feda4fc67d0a4095
會發現,和咱們輸入的 hash 值同樣! 那咱們用一樣的方法手動計算一下 commit 的 hash 值呢:
> (printf "commit %s\0" $(git cat-file commit 82be13e5bf9fafe4db5ad38c76a5c0116e156953 | wc -c); git cat-file commit 82be13e5bf9fafe4db5ad38c76a5c0116e156953) | openssl sha1 82be13e5bf9fafe4db5ad38c76a5c0116e156953
和預期同樣,輸出了咱們在目錄樹中看到的 hash 值,因此,在 git 中,始終使用了這套計算公式來計算 hash:
類型 字節數\0內容
咱們修改一下當前的文件,並提交
> cat foo.txt this is foo and new line
執行git add
後,咱們的目錄樹變成了這樣
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- c9/ | | | `-- da32f4e76824497d02312e46ac0a40e28bef91 | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
多出了一個名爲c9
的文件夾,以及相關文件,其實咱們用git cat-file
就能夠看到,這個就是咱們當前的文件
> git cat-file -p c9da32f4e768 this is foo and new line
這說明,git 根據咱們的最新文件,又建立了一個新的壓縮記錄。這個壓縮文件中是全量的 foo.txt,就算刪掉以前的9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a
文件,對當前的文件也是沒有任何影響的,咱們依然可以得到最新版的全量文件!只是刪除之前的記錄後,回滾就會出現問題。
接着咱們提交這個改動
> git commit -m "update foo.txt" [master 432a5d9] update foo.txt 1 file changed, 1 insertion(+)
而後看咱們的目錄樹
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 03/ | | | `-- 7b10984589cbfe8c6a9c5b84d61592f84fd97a | | |-- 43/ | | | `-- 2a5d918414362c78da50274a1fa0c57b5dc380 | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- c9/ | | | `-- da32f4e76824497d02312e46ac0a40e28bef91 | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
和預期的同樣,生成了43
和03
文件夾,分別存放了此次的commit
記錄和tree
記錄
> git cat-file -t 432a commit > git cat-file -t 037b tree
根據上面的一些操做,咱們對 git 的整套存儲機制已經有了很是清晰的瞭解,咱們能夠大膽的畫出下面這個模型圖
注意:圖中我並無寫出每一個文件的hash,可是要知道,每一個層中的每一個文件都有惟一的一個 hash 做爲文件名,因此永遠不會存在兩個內容相同的文件(就算你的commit信息同樣,可是其所對應的 tree 和 blob 的 hash 都會是不一樣的)!
git 中每一級都是很清晰的分層,各司其職,而後經過 hash 把他們連接起來!
從圖中咱們能夠看到,當前咱們所在的 HEAD 是指向的master
分支,而master
分支又指向某一個指定的提交記錄add bar.txt
,該提交記錄又指向提交時的一個文件目錄樹,這個樹則繼續指向了當時的文件!
這種存儲方式清晰,可靠,擴展性強,在真實文件層,一樣內容的文件,只會存儲一份(由於 blob hash 的計算只和文件內容相關,因此一個文件修改後提交,會增長一個新的 blob,若是將其修改回來,再提交,並不會建立新的 blob,而是複用以前的,由於他們的內容都是如出一轍的,就算文件名不一樣,但內容徹底相同的文件,也會只存儲一份!)
其實經過圖中能夠看到,新建或者切換分支,咱們要作的僅僅是新建一個 branch 文件,而後將該文件指向咱們指定的 commit 便可,就是這麼簡單可靠。
至於 git 的回滾、rebase、merge 等等操做,如今看起來就清晰不少了吧
文章開頭寫的,hooks 能夠幫咱們作不少事情,咱們以前還留有一個叫pre-commit.sample
hooks 沒有刪除,咱們就將其拿來用一下
(由於該文件中有一些示例,若是你不想清空,那就複製一份該文件,並將其末尾的 .sample 去掉)
> copy .git/hooks/pre-commit.sample .git/hooks/pre-commit
編輯.git/hooks/pre-commit
文件,將其清空,並輸入下面的代碼
#!/bin/sh echo "this is pre-commit hooks"
則你在下一次提交的時候,控制檯會打印出上面的文字,在這裏面,能夠寫上一些 lint 的代碼,若是 lint 經過,則容許提交,而後在pre-push
這個 hooks 中運行本地測試,經過才容許提交到服務端,等等(以前在作組件庫的時候,咱們還用 hooks 自增小版本,哈哈哈,很是方便,也不會有小版本失控的狀況,由於更新中版本的時候,會手動重置小版本
如今已經有不少庫可以幫助你執行 hooks 了,如husky
🎉