Git 原理入門

Git 是最流行的版本管理工具,也是程序員的必備技能之一。html

即便每天使用它,不少人也未必瞭解它的原理。Git 爲何能夠管理版本?git addgit commit這些基本命令,到底在作什麼,你說得清楚嗎?git

1、初始化

首先,讓咱們建立一個項目目錄,並進入該目錄。程序員

$ mkdir git-demo-project $ cd git-demo-project
咱們打算對該項目進行版本管理,第一件事就是使用git init命令,進行初始化。
git init


命令只作一件事,就是在項目根目錄下建立一個子目錄,用來保存版本信息。$ git initgit init.git
$ ls .git branches/ config description HEAD hooks/ info/ objects/ refs/

2、保存對象

接下來,新建一個空文件test.txttest.txt
而後,把這個文件加入 Git 倉庫,也就是爲的當前內容建立一個副本。$ touch test.txt test.txt
$ git hash-object -w test.txt e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
上面代碼中,git hash-object命令把test.txt的當前內容壓縮成二進制文件,存入 Git。壓縮後的二進制文件,稱爲一個 Git 對象,保存在.git/objects目錄。
這個命令還會計算當前內容的 SHA1 哈希值(長度40的字符串),做爲該對象的文件名。下面看一下這個新生成的 Git 對象文件。
git hash-objecttest.txt.git/objects
$ ls -R .git/objects .git/objects/e6: 9de29bb2d1d6434b8b29ae775ad8c2e48c5391

上面代碼能夠看到,.git/objects下面多了一個子目錄,目錄名是哈希值的前2個字符,該子目錄下面有一個文件,文件名是哈希值的後38個字符。bash

再看一下這個文件的內容。工具

$ cat .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 

上面代碼輸出的文件內容,都是一些二進制字符。你可能會問,test.txt是一個空文件,爲何會有內容?這是由於二進制對象裏面還保存一些元數據。spa

若是想看該文件原始的文本內容,要用git cat-file命令。指針

$ git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 

由於原始文件是空文件,因此上面的命令什麼也看不到。如今向test.txt寫入一些內容。code

$ echo 'hello world' > test.txt 

由於文件內容已經改變,須要將它再次保存成 Git 對象。htm

$ git hash-object -w test.txt 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 

上面代碼能夠看到,隨着內容改變,test.txt的哈希值已經變了。同時,新文件.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad也已經生成了。如今能夠看到文件內容了。對象

$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello world 

3、暫存區

文件保存成二進制對象之後,還須要通知 Git 哪些文件發生了變更。全部變更的文件,Git 都記錄在一個區域,叫作"暫存區"(英文叫作 index 或者 stage)。等到變更告一段落,再統一把暫存區裏面的文件寫入正式的版本歷史。

git update-index命令用於在暫存區記錄一個發生變更的文件。

$ git update-index --add --cacheinfo 100644 \ 3b18e512dba79e4c8300dd08aeb37f8e728b8dad test.txt 

上面命令向暫存區寫入文件名test.txt、二進制對象名(哈希值)和文件權限。

git ls-files命令能夠顯示暫存區當前的內容。

$ git ls-files --stage 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 test.txt 

上面代碼表示,暫存區如今只有一個文件test.txt,以及它的二進制對象名和權限。知道了二進制對象名,就能夠在.git/objects子目錄裏面讀出這個文件的內容。

git status命令會產生更可讀的結果。

$ git status

要提交的變動:
    新文件:   test.txt 

上面代碼表示,暫存區裏面只有一個新文件test.txt,等待寫入歷史。

4、git add 命令

上面兩步(保存對象和更新暫存區),若是每一個文件都作一遍,那是很麻煩的。Git 提供了git add命令簡化操做。

$ git add --all 

上面命令至關於,對當前項目全部變更的文件,執行前面的兩步操做。

5、commit 的概念

暫存區保留本次變更的文件信息,等到修改了差很少了,就要把這些信息寫入歷史,這就至關於生成了當前項目的一個快照(snapshot)。

項目的歷史就是由不一樣時點的快照構成。Git 能夠將項目恢復到任意一個快照。快照在 Git 裏面有一個專門名詞,叫作 commit,生成快照又稱爲完成一次提交。

下文全部提到"快照"的地方,指的就是 commit。

6、完成提交

首先,設置一下用戶名和 Email,保存快照的時候,會記錄是誰提交的。

$ git config user.name "用戶名" $ git config user.email "Email 地址" 

接下來,要保存當前的目錄結構。前面保存對象的時候,只是保存單個文件,並無記錄文件之間的目錄關係(哪一個文件在哪裏)。

git write-tree命令用來將當前的目錄結構,生成一個 Git 對象。

$ git write-tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d 

上面代碼中,目錄結構也是做爲二進制對象保存的,也保存在.git/objects目錄裏面,對象名就是哈希值。

讓咱們看一下這個文件的內容。

$ git cat-file -p c3b8bb102afeca86037d5b5dd89ceeb0090eae9d 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad test.txt 

能夠看到,當前的目錄裏面只有一個test.txt文件。

所謂快照,就是保存當前的目錄結構,以及每一個文件對應的二進制對象。上一個操做,目錄結構已經保存好了,如今須要將這個目錄結構與一些元數據一塊兒寫入版本歷史。

git commit-tree命令用於將目錄樹對象寫入版本歷史。

$ echo "first commit" | git commit-tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa 

上面代碼中,提交的時候須要有提交說明,echo "first commit"就是給出提交說明。而後,git commit-tree命令將元數據和目錄樹,一塊兒生成一個 Git 對象。如今,看一下這個對象的內容。

$ git cat-file -p c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d author ruanyf 1538889134 +0800 committer ruanyf 1538889134 +0800 first commit 

上面代碼中,輸出結果的第一行是本次快照對應的目錄樹對象(tree),第二行和第三行是做者和提交人信息,最後是提交說明。

git log命令也能夠用來查看某個快照信息。

$ git log --stat c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa commit c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa Author: ruanyf Date: Sun Oct 7 13:12:14 2018 +0800 first commit test.txt | 1 + 1 file changed, 1 insertion(+) 

7、git commit 命令

Git 提供了git commit命令,簡化提交操做。保存進暫存區之後,只要git commit一個命令,就同時提交目錄結構和說明,生成快照。

$ git commit -m "first commit" 

此外,還有兩個命令也頗有用。

git checkout命令用於切換到某個快照。

$ git checkout c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa

git show命令用於展現某個快照的全部代碼變更。

$ git show c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa

8、branch 的概念

到了這一步,還沒完。若是這時用git log命令查看整個版本歷史,你看不到新生成的快照。

$ git log

上面命令沒有任何輸出,這是爲何呢?快照明明已經寫入歷史了。

原來git log命令只顯示當前分支的變更,雖然咱們前面已經提交了快照,可是尚未記錄這個快照屬於哪一個分支。

所謂分支(branch)就是指向某個快照的指針,分支名就是指針名。哈希值是沒法記憶的,分支使得用戶能夠爲快照起別名。並且,分支會自動更新,若是當前分支有新的快照,指針就會自動指向它。好比,master 分支就是有一個叫作 master 指針,它指向的快照就是 master 分支的當前快照。

用戶能夠對任意快照新建指針。好比,新建一個 fix-typo 分支,就是建立一個叫作 fix-typo 的指針,指向某個快照。因此,Git 新建分支特別容易,成本極低。

Git 有一個特殊指針HEAD, 老是指向當前分支的最近一次快照。另外,Git 還提供簡寫方式,HEAD^指向 HEAD的前一個快照(父節點),HEAD~6則是HEAD以前的第6個快照。

每個分支指針都是一個文本文件,保存在.git/refs/heads/目錄,該文件的內容就是它所指向的快照的二進制對象名(哈希值)。

9、更新分支

下面演示更新分支是怎麼回事。首先,修改一下test.txt

$ echo "hello world again" > test.txt 

而後,保存二進制對象。

$ git hash-object -w test.txt c90c5155ccd6661aed956510f5bd57828eec9ddb 

接着,將這個對象寫入暫存區,並保存目錄結構。

$ git update-index test.txt $ git write-tree 1552fd52bc14497c11313aa91547255c95728f37 

最後,提交目錄結構,生成一個快照。

$ echo "second commit" | git commit-tree 1552fd52bc14497c11313aa91547255c95728f37 -p c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa 785f188674ef3c6ddc5b516307884e1d551f53ca 

上面代碼中,git commit-tree-p參數用來指定父節點,也就是本次快照所基於的快照。

如今,咱們把本次快照的哈希值,寫入.git/refs/heads/master文件,這樣就使得master指針指向這個快照。

$ echo 785f188674ef3c6ddc5b516307884e1d551f53ca > .git/refs/heads/master 

如今,git log就能夠看到兩個快照了。

$ git log

commit 785f188674ef3c6ddc5b516307884e1d551f53ca (HEAD -> master) Author: ruanyf Date: Sun Oct 7 13:38:00 2018 +0800 second commit commit c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa Author: ruanyf Date: Sun Oct 7 13:12:14 2018 +0800 first commit 

git log的運行過程是這樣的:

  1. 查找HEAD指針對應的分支,本例是master
  2. 找到master指針指向的快照,本例是785f188674ef3c6ddc5b516307884e1d551f53ca
  3. 找到父節點(前一個快照)c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
  4. 以此類推,顯示當前分支的全部快照

最後,補充一點。前面說過,分支指針是動態的。緣由在於,下面三個命令會自動改寫分支指針。

  • git commit:當前分支指針移向新建立的快照。
  • git pull:當前分支與遠程分支合併後,指針指向新建立的快照。
  • git reset [commit_sha]:當前分支指針重置爲指定快照。
相關文章
相關標籤/搜索