Git 如何存儲數據的

Git 如何存儲數據的.md

Git 使咱們天天使用得最多的工具之一,它是 linux 內核的最先做者 Linus Torvalds 建立的全新版本控制工具,和 linux 同樣的簡單好用,這篇文章就簡單地講一下 git 是如何工做的linux

要求

  • 知道 Git
  • 摘要算法的做用(MD五、sha-1
  • 一些簡單的 Linux 命令

你將瞭解

  • .git 目錄結構
  • git 如何存儲文件

Demo 中用到的工具

  • VS Code:一款通用編輯器
  • iTerm:macOS 上的命令行軟件
  • watch:類 Unix 上的命令行工具,能夠定時重複執行指定命令並輸出
  • tree:類 Unix 上的命令行工具,能夠輸出指定目錄的目錄樹結構

本地配置

1. 在本地安裝 Git

https://git-scm.com/下載安裝便可git

2. 先在任意位置建立一個空文件夾

我在個人桌面上建立了一個名爲 git_demo 的文件夾github

3. 在命令行中切換到該目錄下

4. 監控目錄變化

我這裏是直接拆分窗口,便於顯示,並導航到一樣的目錄算法

在右側窗口中,輸入如下命令,用來監控目錄變化緩存

watch -d -n 1 tree --charset=ascii -aF

該命令綜合了兩個工具,一個是watch一個是tree,經過 watch 來定時(上面指定每隔 1 秒)執行一次後面的命令,咱們就可以看到實時的目錄變化了bash

補上一個動圖版本編輯器

開始 Demo

1. Git init

在左側命令行中執行熟悉的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.
  • hooks 這裏存放本地的 git hooks

    他的做用就是在你執行一些特定 git 命令(好比 git commit)的時候,順帶執行你指定的 bash 命令,很強大的功能。好比咱們想在提交前執行代碼測試,咱們就能夠去編輯 hooks 裏面的pre-commit.sample這個文件,而且去掉文件名末尾的.sample,那在你下次執行git commit的時候,就會先運行這個 bash,只有當整個 bash 執行成功後(退出碼爲 0),纔會執行你剛纔的git commit.

  • info 存放一些默認的配置文件

    默認的已經存放了一個名爲exclude的文件,其實就是一個.gitignore文件

  • objects 這裏真實地存放着咱們代碼的備份,一下子會詳細說明的
  • refs 這個也是很是關鍵的一個文件夾,存放着咱們全部的分支信息

這個文件夾內,最重要的就是HEAD文件,objects文件夾,refs文件夾,git 經過先訪問HEAD文件,找到咱們當前工做的分支,而後再去refs文件夾中找到對應的分支描述,這個描述文件正是指向了objects文件夾內的某一個文件,再經過這個文件,咱們就能獲得當前分支當前版本的全部源碼了!

上面看了,HEAD文件其實就只有一行,指向了refs目錄的一個文件

咱們就來探祕一下refs文件夾和objects文件夾究竟是在作什麼

2. 新建一個文件

爲了不過多的文件影響,咱們先將 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文件

3. 將文件添加進緩存區

其實就是咱們無比熟悉的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 是如何存儲咱們的文件了,那咱們提交一次代碼,看會有什麼變化

4. 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

5. 進行一次 commit

很常規,咱們就執行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 目錄中多出了兩個文件夾82f8以及相應的文件,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內容

6. 修改當前文件並提交

咱們修改一下當前的文件,並提交

> 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

和預期的同樣,生成了4303文件夾,分別存放了此次的commit記錄和tree記錄

> git cat-file -t 432a
commit
> git cat-file -t 037b
tree

7. 提早總結

根據上面的一些操做,咱們對 git 的整套存儲機制已經有了很是清晰的瞭解,咱們能夠大膽的畫出下面這個模型圖

注意:圖中我並無寫出每一個文件的hash,可是要知道,每一個層中的每一個文件都有惟一的一個 hash 做爲文件名,因此永遠不會存在兩個內容相同的文件(就算你的commit信息同樣,可是其所對應的 tree 和 blob 的 hash 都會是不一樣的)!

git 中每一級都是很清晰的分層,各司其職,而後經過 hash 把他們連接起來!

從圖中咱們能夠看到,當前咱們所在的 HEAD 是指向的master分支,而master分支又指向某一個指定的提交記錄add bar.txt,該提交記錄又指向提交時的一個文件目錄樹,這個樹則繼續指向了當時的文件!

這種存儲方式清晰,可靠,擴展性強,在真實文件層,一樣內容的文件,只會存儲一份(由於 blob hash 的計算只和文件內容相關,因此一個文件修改後提交,會增長一個新的 blob,若是將其修改回來,再提交,並不會建立新的 blob,而是複用以前的,由於他們的內容都是如出一轍的,就算文件名不一樣,但內容徹底相同的文件,也會只存儲一份!)

8. 分支切換

其實經過圖中能夠看到,新建或者切換分支,咱們要作的僅僅是新建一個 branch 文件,而後將該文件指向咱們指定的 commit 便可,就是這麼簡單可靠。

至於 git 的回滾、rebase、merge 等等操做,如今看起來就清晰不少了吧

9. 打包送的 hooks

文章開頭寫的,hooks 能夠幫咱們作不少事情,咱們以前還留有一個叫pre-commit.samplehooks 沒有刪除,咱們就將其拿來用一下

(由於該文件中有一些示例,若是你不想清空,那就複製一份該文件,並將其末尾的 .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

🎉

相關文章
相關標籤/搜索