1、什麼是git
git的官方定義:Git is a fast, scalable, distributed revision control system with an unusually rich command set that provides both high-level operations and full access to internals.
可以看出,git是一個具有豐富命令集的版本控制系統,它的特點就是快速、可擴展以及分布式。
而git實際上是一個簡單內容記錄工具(the stupid content tracker)。從根本上來講,git是一套內容尋址 (content-addressable) 文件系統,在此之上提供了一個版本控制系統的用戶界面。
1.1 an unusually rich command set
在當前版本裡,git一共有158個命令,但是常用命令只有10多個,其他命令可以視為底層命令。由於git最開始被設計的時候用來作為the stupid content tracker,只是現在被最廣泛地用來作為revision control system,但是實際上git也可以被用來做其它事情。因此,git不僅一開始提供了功能齊全的底層命令,而且還提供了許多友好的高層命令。
git命令分為兩種:
Procelain(高層命令)
user-friendly commands: init, add, commit, push, checkout, branch, merge, etc.
Plmbing(底層命令)
bunch of verbs that do low-level work: hash-object, update-index, write-tree, etc.
1.2 revision control system
版本控制系統,學名:software change and configuration management system(SCM)。它需要具有的特點如下:
記錄和控制軟件的變化點和版本號
能清楚知道什麼東西被改了,被誰改了
在多人完成的大項目中,能更加容易的合作和管理文件
最重要的是,要有回滾功能
在實際生活中,版本控制就被經常用到,例如寫論文時的版本控制。但是,這只是簡單的本地版本控制。在多人的項目合作中,這就顯得力不從心了。因為使用這種方法時,首先需要大量的拷貝工作,以保證能夠回滾嘛;其次當多人更改同一項目出現寫寫沖突情況時,雖然Linux提供diff和patch命令,但是解決起來肯定極其痛苦。
因此,版本控制系統,核心就是讓在不同系統上的開發者協同工作,需要解決的主要問題就是防止各種人一塊寫最後寫亂套的情況。
2、git有何不同
2.1 集中式版本控制系統
首先,談談傳統的集中式版本控制系統。
集中式版本控制系統中,版本庫是集中存放在中央服務器(server),每個開發者都作為客戶端(client)。協同工作的開發者都通過客戶端連到中央服務器,取出(checkout)最新的文件或者提交(commit)更新。它的主要特點如下:
每次checkout拿到的都是當前版本庫最新的一個快照狀態(snapshot)。它並不知道版本庫之前的歷史狀態信息。
每次commit都會更新中央版本庫。
為了保證不丟失數據,在每次commit時都需要re-sync。
而集中式版本控制系統的缺點,則有如下幾點:
1. 對於每次commit操作,都需要re-sync;
對於集中式版本控制系統,在多人協作中,可以認為如果commit越早,麻 煩也就越少。
從這個角度上看,可以認為git做了兩步commit操作,第一次為commit,第二次為pull,只有pull才需要re-sync。
2. 單點故障
在沒有commit之前所有文件都在於client端的本地文件,如果此時不小心刪除掉了一些文件,除非重新訪問server進行回滾操作,否則就真的刪除掉
當server沒有備份並且宕掉了,就算能恢復當前版本的數據,但是之前的歷史數據信息是恢復不了的。
3. 每次工作的時候,需要聯網;
4. 速度慢;
大部分時間都是需要client和server進行溝通協作的。
2.2 分布式版本控制系統
我們再來看看git,它是一個分布式版本控制系統。
與傳統的集中式版本控制系統相比,git最大的不同在於,它是分布式版本控制系統。分布式意味著,不止服務器保存有version database,每個客戶端也保存有version database。git的主要特點如下:
每次clone都真正的拷貝所有的數據。
在初始clone之後,大多數的操作都是在本地進行的。
而git版本控制系統的主要優點如下三點:
1. Fast:
除了第一次clone的時候慢,之後的操作都比較快。因為大多數操作都是在本地進行的,不需要聯網。
2. Scalable:
你可以對本地的version database做任何你想做的事情,他並不會對其他人 造成任何影響。
3. Distributed:
在client端,肯定不法徹底避免單點故障,但是可以減少這樣可能性。因為本地保存有version database,所以當本地不小心丟失文件時,往往可以本地出錯本地解決。
在server壞掉的時候,可以根據client裡面的version database進行恢復,甚至可以把client當成一個新的server。
3、git如何存儲內部數據
從上一小節,我們知道git是一個分布式版本控制系統,無論是服務端還是客戶端都擁護一份自己的version database。那麼本小節,首先看看version database到底是什麼。然後,分別深入看看git init、git add、git commit這個三個常用命令具體的工作過程。
version database
git的version database到底是什麼?
1. 本質上,是一套內容尋址文件系統(Content Addressable Filesystem)。
2. 實現上,是一個簡單的KV數據庫
3. Key: sha-1 hash(everything is hashed)
20 bytes, 40 hex, 160 bit, 2.9e48 distinct keys
How would git handle a SHA-1 collision on a blob?
4.Value: binary files
Commits : actual git commits
Trees : directories(structure of file system)
Blobs : contents of files/data
git以一種類似UNIX文件系統但更簡單的key-value方式來存儲內容。key是對文件內容的哈希值。value則包含有commit object、tree object以及blob object三種對象。
所有內容以tree或blob對象存儲,其中tree對象對應於UNIX中的目錄,blob對象則大致對應於inodes或文件內容。commit對象則可以看作是對當前版本的一個快照。
一個單獨的tree對象包含一條或多條tree記錄,每一條記錄含有一個指向blob或子tree對象的SHA-1指針,並附有該對象的權限模式、類型和文件名信息。
接下來一步一步,看來git flow裡到底發生了什麼:
3.1 git init命令
git init主要是創建了四個文件夾:.git、.git/refs/heads、.git/refs/tags以及.git/objects,和一個文件.git/HEAD,並把初始化./git/HEAD裡面的內容為指向master branch的HEAD。
那麼如何用普通的Linux命令實現git init呢?答案其實很簡單:
mkdir .git mkdir -p .git/refs/heads mkdir -p .git/refs/tags mkdir -p .git/objects touch .git/HEAD echo "ref:refs/heads/master" > .git/HEAD
以上便是git init, without git init的具體實現做法。其中,objects目錄存儲所有數據內容,refs目錄存儲指向數據(分支)的提交對象的指針,HEAD文件指向當前分支。
3.2 git add命令
git add做了兩件事情:
第一,把文件寫成blob object作為value,並把文件內容的hash值作為key;
第二,更新了index文件,把文件放入了暫存區(staging area)。
echo "Hello World" > hello.txt git add .
上面的操作可以用下面底層語句實現:
echo "Hello World" | git hash-object -w --stdin git update-index --add --cacheinfo 100644 557db03de997c86a4028e1ebd3a1ceb225be238 hello.txt git checkout -- hello.txt
第一個命令中,參數-w指示hash-object命令存儲(數據)對象,若不指定這個參數該命令僅僅返回鍵值。該命令輸出長度為40個字符的SHA-1哈希值校驗和,並創建以它的前2個字符為名稱的子目錄,剩下38個字符作為文件命名(保存至子目錄下)。
第二個命令,update-index為一個單獨文件創建一個index。更新完staging area後,由於僅僅Index裡有,但是本地仍沒有,如果git status,所以會顯示為deleted。此時,使用第三條命令可以把repository進行checkout出來。
3.3 git commit命令
我們進行第一次git commit操作。
git commit -m "First Commit"
此時會多出兩個文件。
第一個是commit object,它實際上是對於跟蹤項目內容的一次快照,裡面的格式內容有:指明了該時間點項目快照的頂層樹對象、作者/提交者信息以及提交注釋信息;
第二個則是那個被指向tree object,它實際上一個文件根目錄,裡面指向一個blob object,而這個blob object便是之前git add時的內容。
雖然執行git log命令可以查看完整的歷史信息並以此找到文件,那麼問題來了,系統又是如何找到commit log的呢? 答案是:refs!
在.git/HEAD裡存的是一個ref,ref是一個file,這個file裡面又存了一個commit的hash值,從而便可以找到了當前的base版本。
我們嘗試進行第二次git commit操作。 首先,git add一個簡單的腳本文件hello.sh。
/*************hello.sh的具體內容如下:
!/bin/sh echo "Hello World" **************/ git add hello.sh
此時,git add只會增加一個blob object,而不會增加多一個tree object。也就是說,在add的時候並不寫目錄,只是更新了暫存區,在index裡會保存目錄信息。因為在git commit操作前,可以進行多次git add操作。
git commit -m "Second Commit"
此時,會多出三個文件。第一個是commit object,第二個是根目錄的tree object,第三個是文件目錄的tree object。
通過下圖,可以發現commit object中還帶有個parent指針,它指向上一次的commit object。最重要的是,它還更新自身的refs/heads/master信息;
通過命令git ls-files --stage查看staging area信息。你還可以發現,暫存區裡只存儲blob object,並不存儲tree object和commit object。
4、branch/merge在git內部是如何工作
相對於git init、git add、git commit這三個命令,git branch和git merge則顯得十分的小巧機智。結合下列幾幅圖,能更輕松的理解這兩個命令。
4.1 branch操作
在git裡,branch操作,實際上僅僅增添一個41-bytes的引用文件(在refs/heads/目錄底下)。可以看出,這樣的操作是極其的簡單廉價的,示意圖如下:
同理,在git裡,checkout操作,實際上也僅僅是更改了引用文件的指針而已。
4.2 merge操作
最後,我們來分析一下,git裡的merge操作。它的操作流程和示意圖如下:
1. 計算出當前branch和公共branch的diff;
這步操作的速度很快。因為只需要去比較文件的hash值是否匹配即可。
2. 應用該diff去訂正本地文件;
這步操作並不會覆蓋分支裡之前的版本數據。
3. 解決所有的沖突;
當merge操作完成後,示意圖如下:
首先,可以發現,這是會新建一個mater處的commit object,而它有兩個parent指針,分別指向之前兩個branch的commit object。
其次,如果不小心做了一個bad merge,而又不想讓他人知道時,其實只需要更改下master和feature指針的hash信息即可。
最後,通過branch和merge操作,你會發現在git的世界裡,它會盡量不讓你做刪除的事情。
5、小結
Git is the stupid content tracker.
You can represent renames on top of git - git itself really doesn't care. In many ways you can just see git as a filesystem - it's content-addressable, and it has a notion of versioning, but I really really designed it coming at the problem from the viewpoint of a _filesystem_person (hey, kernels is what I do), and I actually have absolutely _zero_interest in creating a traditional SCM system. -- by Linus Torvalds
我們應該如何看待並理解git呢?本質上,git是一套文件尋址系統。
雖然git具有版本控制的功能,但是在Linus最初設計git的時候,完完全全是從一個文件系統的角度進行設計和實現這個產品的。所以說,git本質上仍然是一個文件系統,只是它很適合地被用作版本控制系統。