用了5年的Git,你竟然還不曉得它的實現原理!

2021-12-23 Java面試那些事兒

來源:https://zhuanlan.zhihu.com/p/53750883

哈嘍,各位新來的小夥伴們,大家好!由於公眾號做了改版,為了保證公眾號的資源能準時推送到你手裡,大家記得將咱們的公眾號 加星標置頂 ,在此真誠的表示感謝~

正文如下:

越了解事物的本質就越接近真相。我發現學習Git內部是如何工作的以及Git的內部數據結構這部分內容,對於理解Git的用途和強大至關重要。若你理解了Git的思想和基本工作原理,用起來就會知其所以然,遊刃有餘。這是Git系列的第一篇,主要會介紹Git的特點以及內部數據結構設計,和完成一次完整提交流程的時候數據是如何變化的。

幾乎所有操作都是本地執行

每一個clone都是整個生命周期的完整副本

init, add, commit, branch, merge.

# Git Version Database是什麼?Git是一個內容尋址文件系統。這意味著,Git的核心部分是一個簡單的鍵值對資料庫(key-value data store)。你可以向該資料庫插入任意類型的內容,它會返回一個鍵值,通過該鍵值可以在任意時刻再次檢索該內容。而這些數據全部是存儲在objects目錄裡。key是一個hash,hash前兩個字符用於命名子目錄,餘下的38個字符則用作文件名。如果了解tree樹的朋友應該會想明白之所以這樣處理是因為檢索優化策略,提高文件系統效率(如果把太多的文件放入同一個目錄中,一些文件系統會變慢)。而這個hash的內容(即hash對應的Value)有四種對象類型,commit(提交),tree(目錄樹),blob(塊),tag(標籤)。note:可以理解成Commit = Tree + Blob的snapshot什麼是SHA-1:SHA-1(安全散列函數),是一種密碼散列函數,美國國家安全局設計,並由美國國家標準技術研究所發布為聯邦數據處理標準。SHA-1可以生成一個被稱為消息摘要的160位(20位元組)散列值,散列值通常的呈現形式為40個十六進位數。用js來理解就是一個純函數,輸入一定輸出也一定,相同的輸入一定有相同的輸出。不相同的輸入一定有不同的輸出(不考慮碰撞 ,比彗星撞擊地球的概率還低)。

在工作目錄中修改文件。

暫存文件,將文件的快照放入暫存區域。

提交更新,找到暫存區域的文件,將快照永久性存儲到Git倉庫目錄。

$ git init$ git add .$ git commit

在我們看這三個命令到底做了什麼之前,先來了解一下幾個概念:我們先用Git init來初始化一個項目,並查看項目的目錄結構。

$ git init demo1 && cd demo1$ tree .git.git├── HEAD├── 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    └── tags

description文件僅供GitWeb程序使用。config文件包含項目特有的配置選項。info目錄包含一個全局性排除文件,用以放置那些不希望被記錄在.gitignore文件中的忽略模式。hooks目錄包含客戶端或服務端的鉤子腳本,這些我們暫時都無需關心。最重要的是:HEAD文件、(尚待創建的)index文件,和objects目錄、refs目錄。這些條目是Git的核心組成部分。objects目錄存儲所有數據內容(hash);refs目錄存儲指向數據(分支)的提交對象的指針(commit hash);HEAD文件指示目前被檢出的分支(refs目錄內的分支名);index 文件保存暫存區信息(git ls-files --stage命令查看當前暫存區信息)。下面我們就用底層命令來實現git init指令(另創建一個demo2目錄)。mkdir -p參數是能直接創建一個不存在的目錄下的子目錄:

$ mkdir -p .git/refs/heads .git/refs/tags .git/objects$ echo 'ref: refs/heads/master' > .git/HEAD

$ echo 'hello git' > index.txt$ git add index.txt

執行完這兩句指令後我們再來看.git文件夾發生了什麼變化(為了顯示效果,簡化目錄結構,之後tree 都忽略hooks文件夾)

.git├── HEAD├── config├── description├── index├── info│   └── exclude├── objects│   ├── 8d│   │   └── 0e41234f24b6da002d962a26c2495ea16a425f│   ├── info│   └── pack└── refs    ├── heads    └── tags

可以看到多了一個index文件,並且objects目錄裡面多了一個8d的文件夾,裡面有一個0e41開頭的文件、那這個8d0e4這個是什麼呢?其實這個就是index.txt文件內容的hash。還記得嘛,剛才寫入文件內容是hello git,我們來手動輸出這個內容的hash。

$ echo 'hello git' | git hash-object --stdin$ 8d0e41234f24b6da002d962a26c2495ea16a425f

可以通過cat-file命令從Git那裡取回數據。為cat-file指定-p選項可指示該命令自動判斷內容的類型,並為我們顯示格式友好的內容:

$ git cat-file -p 8d0e$ hello git

為cat-file指定-t選項可以查看文件的類型:

$ git cat-file -t 8d0e$ blob

文件內容做一個hash存成blob object

把index放入到Staging Area

當為index.txt創建一個對象的時候,git並不關心index.txt的文件名,git 只關心文件裡面的內容。按照這個思路,我們用底層命令來實現一下git add指令。
$ echo 'hello git' | git hash-object -w --stdin
$ git update-index --add --cacheinfo 100644 8d0e41234f24b6da002d962a26c2495ea16a425f index.txt

-w選項指示hash-object命令存儲數據對象;若不指定此選項,則該命令僅返回對我們指定的文件模式為100644,表明這是一個普通文件。其他選擇包括:100755,表示一個可執行文件;120000,表示一個符號連結。因為並沒有去創建這個index.txt文件, 所以這邊提示已經刪除了,執行git checkout -- index.txt取出文件。可以看到已經成功用底層命名實現了git add的功能。Git是通過tree對象來跟蹤文件的路徑名的。當使用git add命令時,git會給添加的文件內容創建一個blob對象,但是這個時候並不會創建tree對象。而只是更新索引,索引在.git/index中,它跟蹤文件的路徑名和相對應blob,每次執行git add 、git rm 、 git mv 的時候,git都會更新索引,我們可以通過命令git ls-files --stage來查看當前的索引信息。

$ git ls-files --s$ 100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0 index.txt

執行git commit -m 'init-1'後,查看tree結構,發現object 多出了兩個文件:

.git├── COMMIT_EDITMSG├── HEAD├── config├── description├── index├── info│   └── exclude├── logs│   ├── HEAD│   └── refs│       └── heads│           └── master├── objects│   ├── 75│   │   └── 0d7c0f7f998d3e2ce2d71ec801902f69bf6a39│   ├── 88│   │   └── bc066ebf3d864e34297f7051a0ded16e49813a│   ├── 8d│   │   └── 0e41234f24b6da002d962a26c2495ea16a425f│   ├── info│   └── pack└── refs    ├── heads    │   └── master    └── tags$ git log$ commit 750d7c0f7f998d3e2ce2d71ec801902f69bf6a39 (HEAD -> master)

查看這個commit 的文件類型,可以看到這是一個commit:

$ git cat-file -t 750d$ commit
$ git cat-file -p 750d$ tree 88bc066ebf3d864e34297f7051a0ded16e49813a

但是多出來的88bc是什麼呢,其實就是當前目錄的tree對象,所以Git是在commit的時候才創建tree對象的(其實是把索引轉化成tree對象)。

$ git cat-file -t 88bc$ tree
$ git cat-file -p 88bc$ 100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f index.txt

$ cat .git/HEAD$ ref: refs/heads/master

$ cat .git/refs/heads/master$ 750d7c0f7f998d3e2ce2d71ec801902f69bf6a39

所以整個指向關係就是:HEAD裡面的內容是當前的ref,而當前ref的內容是commit hash,commit對象內容是tree hash,tree對象的內容是文件夾/文件信息,而blob對象存儲著文件的具體內容。這樣當完成一次提交的時候,整個狀態的對應關係也是確定的,所以說commit對象就是當前系統的snapshot。

熱門推薦:

相關焦點

  • git底層原理,從常見操作解釋git的底層原理,再也不怯
    當然你也可以像使用SVN一樣使用一個中心倉庫來交換信息(在項目人員較少時常用這種方式),但是這種工作方式和SVN也不盡全部相同,它們的區別在於,使用git進行開發的人員,每一個節點(每一個電腦上的本地倉庫)都擁有完整的倉庫,在失去與中心倉庫的連接時也可以工作(先提交到本地倉庫)。三、存儲實現原理(Git對象)在git中以存儲鍵值對(key-value)的方式來存儲文件。
  • 這才是真正的Git——Git內部原理揭秘!
    本文作者:lzaneli,騰訊 TEG 前端開發工程師本文以一個具體例子結合動圖介紹了Git的內部原理,包括Git是什麼儲存我們的代碼和變更歷史的、更改一個文件時,Git內部是怎麼變化的、Git這樣實現的有什麼好處等等。通過例子解釋清楚上面這張動圖,讓大家了解Git的內部原理。如果你已經能夠看懂這張圖了,下面的內容可能對你來說會比較基礎。
  • 搞懂 Git 工作原理,遇到問題不再瞎矇
    如果有回答不出的,那麼建議還是往下仔細看看文章吧~三大分區 我們首先用一張圖來理解工作區、暫存區和倉庫的位置:我們先看由下而上的路徑,首先工作區>就是我們當前的文件目錄,我們改完代碼,用git add命令把當前文件加入暫存區,然後git commit把暫存區生成的快照提交到本地倉庫,最後再用git push命令把本地倉庫的提交複製到遠程倉庫,也就是Github之類的在線倉庫
  • 圖文詳解 Git 工作原理
    如果你稍微理解Git的工作原理,這篇文章能夠讓你理解的更透徹。基本用法上面的四條命令在工作目錄、暫存目錄(也叫做索引)和倉庫之間複製文件。git add files  #把當前文件放入暫存區域git commit     #給暫存區域生成快照並提交git reset – files  #用來撤銷最後一次git add files,你也可以用git reset撤銷所有暫存區域文件git checkout – files  #把文件從暫存區域複製到工作目錄
  • 7000+ 字帶你全面搞懂 Git 命令+原理!
    Index/Stage:暫存區,一般存放在 .git目錄下,即.git/index,它又叫待提交更新區,用於臨時存放你未提交的改動。比如,你執行git add,這些改動就添加到這個區域啦。Repository:本地倉庫,你執行git clone 地址,就是把遠程倉庫克隆到本地倉庫。
  • 這才是真正的Git——Git實用技巧
    rebase -i 是個很實用且應用廣泛的工具,希望大家都學會它的使用。它還可以用來修改commit信息,拋棄某些commit,對commit進行排序等等。具體命令如下,操作方式跟動圖一致,都是在vim裡面進行編輯。這裡不展開,感興趣的同學可以自己操作一下。
  • 帶你深入理解 Git 原理
    正如變基一節中闡述的,保持一個清晰的歷史記錄很重要,這就是為什麼我建議無論何時你執行git pull,最好選擇執行git pull -r你也可以告訴git在使用git pull用 rebase而不是merge 作為它的默認方式,通過pull.rebase指令如git config --global pull.rebase true
  • 老哥:你竟然不知道如何用git來統計代碼?
    作者:Jartto    來源:http://1t.click/tHf當我們維護一個開源項目的時候,你肯定想知道哪些人比較活躍,哪些人貢獻比較多。這時候就需要一個簡單易用的工具,下面我來介紹幾款。lines: %s\n", add, subs, loc }' -2、貢獻值統計:git log --pretty='%aN' | sort -u | wc -l3、查看排名前 5 的貢獻者:git log --pretty='%aN' | sort | uniq
  • Git 內部原理圖解——對象、分支以及如何從零開始建倉庫
    但是我深刻地意識到,理解 Git 的工作原理在很多情況下都非常有用——不管是解決合併衝突、進行有趣的變基(rebase)操作,還是在某些東西變得有點不對勁的時候。如果你有足夠的  git  經驗,對  git pull、git push、git add  或  git commit  這些命令得心應手,你會從本文中獲益。不過,為了確保我們在  git  的原理(尤其是本文上下所使用的術語)上步調一致,我們將從概覽開始。
  • Python 命令行之旅:使用 docopt 實現 git 命令
    若你仍在使用 Python 2,請注意兩者之間語法和庫的使用差異哦~二、git 常用命令 當你寫好一段代碼或增刪一些文件後,會用如下命令查看文件狀態:git status確認文件狀態後,會用如下命令將的一個或多個文件(夾)添加到暫存區:git add [pathspec [pathspec ...]]然後使用如下命令提交信息:git commit
  • Git分支原理命令圖文解析
    如果我們創建一個新的分支,child,它和master共同指向A,這時,如果我們向child分支提交更新B,我們會發現child指向B,而master依然指向A。無論我們在child分支進行了任何開發,只要回到master分支,就能恢復到更新A的數據狀態了。 在圖片裡,我們還注意到有一個head指針,一般來說,它會指向我們目前所在的工作分支。
  • 如何在 Git 裡撤銷(幾乎)任何操作
    你可能已經熟悉了 git log 命令,它會顯示 commit 的列表。 git reflog 也是類似的,不過它顯示的是一個 HEAD 發生改變的時間列表.一些注意事項:它涉及的只是 HEAD 的改變。
  • Git 看這一篇就夠了
    原本 Linux 內核的版本控制系統是用的 BitKeeper,然而 2005 年,BitMover 公司不再讓 Linux 開發團隊免費使用了。。Linus 一聽,不給用了?老子自己寫!還有那些年打死都不再改的畢業論文:畢業論文最終版畢業論文最最終版畢業論文最最最終版畢業論文最最最最終版畢業論文最終不改版畢業論文最終真不改版畢業論文最終真真不改版畢業論文最終打死不改版畢業論文最終打死不改版 2...
  • 你可能不太會用的10個Git命令
    我們將了解該如何用 Git 進行檢查、刪除和整理操作。我們還將介紹如何用 Bash 別名和 Git 編輯器配置來逃避 Vim 以節省時間。如果你不熟悉基本的 git 命令,在閱讀本文前可以先參考我之前寫的關於 Git 工作流程的文章。
  • 一篇文章,教你學會Git
    以上包括一些簡單而常用的命令,但是先不關心這些,先來了解下面這4個專有名詞。暫存區標記了你當前工作區中,哪些內容是被git管理的。當你完成某個需求或功能後需要提交到遠程倉庫,那麼第一步就是通過 git add 先提交到暫存區,被git管理。保存了對象被提交 過的各個版本,比起工作區和暫存區的內容,它要更舊一些。
  • 我說小夥子,你死記Git命令,不好使
    ,首先工作區就是我們當前的文件目錄,我們改完代碼,用git add命令把當前文件加入暫存區,然後git commit把暫存區生成的快照提交到本地倉庫,最後再用git push命令把本地倉庫的提交複製到遠程倉庫
  • 你可能不太會用的 10 個 Git 命令
    我們將了解該如何用 Git 進行檢查、刪除和整理操作。我們還將介紹如何用 Bash 別名和 Git 編輯器配置來逃避 Vim 以節省時間。如果你不熟悉基本的 git 命令,在閱讀本文前可以先參考我之前寫的關於 Git 工作流程的文章。
  • 困擾你的Git操作?
    學習前端,離不開 Git 的使用,面試時也是一個常考的話題,在日常開發中,也困擾我們許久,下面就讓我們一起走進它~我將從以下幾點進行介紹, 準備好走進 Git 的世界了嘛~高頻詞彙的含義git 常用指令幾種常見 git 操作流程高頻詞彙的介紹git 操作博大精深,細節很多,對於日常開發的我們而言,熟識幾條常用的操作便可解決日常問題
  • 我到底應該用git-merge還是git-rebase呢?
    本文一共3165字,將通俗的解釋git中兩種合併策略——merge和rebase的不同、運用方式和使用的場景,並輔以我在工作中的運用來介紹;什麼是git merge?git merge是我們在git操作中頻繁會用到的一個命令,它主要實現的功能便是為我們進行分支代碼的合併,也就是將兩個或兩個以上的開發歷史合併在一起的操作。
  • 詳細介紹下git中的多種撤銷
    命令:git revert原理:git revert 會產生一個新的 commit,它和指定需要撤銷的 commit的SHA 是相反的。例如:你在本地執行了 git commit -m "comment5",但是一個單詞拼寫錯誤了,其實應該是 「commit5″。