Git有什么特點?
?
-
fast,scalable,distributed revision control system(快速,可擴展的分布式版本控制系統)
?
-
-
幾乎所有操作都是本地執行
-
每一個clone都是整個生命周期的完整副本
-
?
-
the stupid content tracker(只是一個內容追蹤器)
?
?
-
-
Git追蹤的是內容而不是文件
-
如果兩個文件的內容相同,無論是否在相同的目錄,Git在對象庫里只保存一份blob對象
-
-
Immutable(不可變性)
?
-
-
Git版本庫中存儲的數據對象均為不可變的,一旦創建數據對象并放入了數據庫中,它們便不可修改。這也意味著存儲在版本數據庫中的整個歷史也是不可變的。
-
?
-
Porcelain(高層命令)
?
-
-
init, add, commit, branch, merge.
-
?
-
Plumbing(底層命令)
?
-
-
hash-object, update-index, write-tree.
-
?
每一個Client端都可以是Server
?
Git Version Database是什么?
?
Git是一個內容尋址文件系統。這意味著,Git的核心部分是一個簡單的鍵值對數據庫(key-value data store)。你可以向該數據庫插入任意類型的內容,它會返回一個鍵值,通過該鍵值可以在任意時刻再次檢索該內容。而這些數據全部是存儲在objects目錄里。key是一個hash,hash前兩個字符用于命名子目錄,余下的38個字符則用作文件名。如果了解tree樹的朋友應該會想明白之所以這樣處理是因為檢索優化策略,提高文件系統效率(如果把太多的文件放入同一個目錄中,一些文件系統會變慢)。而這個hash的內容(即hash對應的Value)有四種對象類型,commit(提交),tree(目錄樹),blob(塊),tag(標簽)。
?
Git基本概念:
?
-
Content addressable filesystem(內容尋址文件系統)
-
Simple key-value data store(鍵值對數據)
-
Key:SHA-1散列(hash,哈希)
?
-
-
Everything is hash
-
這是一個由40個十六進制字符(0-9和a-f)組成字符串
-
?
-
Value:binary files
?
-
-
Commit:Actual git commits(提交)
-
Tree:Directoy(目錄樹)
-
Blob:file content(文件內容)
-
?
note:可以理解成Commit = Tree + Blob的snapshot
?
什么是SHA-1:SHA-1(安全散列函數),是一種密碼散列函數,美國國家安全局設計,并由美國國家標準技術研究所發布為聯邦數據處理標準。SHA-1可以生成一個被稱為消息摘要的160位(20字節)散列值,散列值通常的呈現形式為40個十六進制數。用js來理解就是一個純函數,輸入一定輸出也一定,相同的輸入一定有相同的輸出。不相同的輸入一定有不同的輸出(不考慮碰撞 ,比彗星撞擊地球的概率還低)。
?
Git到底是如何工作呢?
?
我們知道最簡單的git flow主要有三步:
?
-
在工作目錄中修改文件。
-
暫存文件,將文件的快照放入暫存區域。
-
提交更新,找到暫存區域的文件,將快照永久性存儲到Git倉庫目錄。
?
對應高層命令是這樣的:
$?git?init
$?git?add?.
$?git?commit
在我們看這三個命令到底做了什么之前,先來了解一下幾個概念:
?
?
-
Working Directory:工作區(工作目錄)
-
Stageing Area (Index):暫存區
-
Repository:倉庫區(本地倉庫)
?
Git init
?
我們先用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
?
可以看到已經成功初始化了一個Git項目。
?
git add
?
$?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
git add做了兩件事情:
?
-
文件內容做一個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
?
執行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
這個時候再看HEAD:
$?cat?.git/HEAD
$?ref:?refs/heads/master
繼續查看refs/heads/master:
$?cat?.git/refs/heads/master
$?750d7c0f7f998d3e2ce2d71ec801902f69bf6a39
所以整個指向關系就是:HEAD里面的內容是當前的ref,而當前ref的內容是commit hash,commit對象內容是tree hash,tree對象的內容是文件夾/文件信息,而blob對象存儲著文件的具體內容。這樣當完成一次提交的時候,整個狀態的對應關系也是確定的,所以說commit對象就是當前系統的snapshot。
?
?
再來回顧下一次完整的提交流程:
?