Git 是最流行的版本管理工具,也是程序員的必備技能之一。html
即便每天使用它,不少人也未必瞭解它的原理。Git 爲何能夠管理版本?git add
、git commit
這些基本命令,到底在作什麼,你說得清楚嗎?git
首先,讓咱們建立一個項目目錄,並進入該目錄。程序員
$ 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/
接下來,新建一個空文件test.txt
。test.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
文件保存成二進制對象之後,還須要通知 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
,等待寫入歷史。
上面兩步(保存對象和更新暫存區),若是每一個文件都作一遍,那是很麻煩的。Git 提供了git add
命令簡化操做。
$ git add --all
上面命令至關於,對當前項目全部變更的文件,執行前面的兩步操做。
暫存區保留本次變更的文件信息,等到修改了差很少了,就要把這些信息寫入歷史,這就至關於生成了當前項目的一個快照(snapshot)。
項目的歷史就是由不一樣時點的快照構成。Git 能夠將項目恢復到任意一個快照。快照在 Git 裏面有一個專門名詞,叫作 commit,生成快照又稱爲完成一次提交。
下文全部提到"快照"的地方,指的就是 commit。
首先,設置一下用戶名和 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(+)
Git 提供了git commit
命令,簡化提交操做。保存進暫存區之後,只要git commit
一個命令,就同時提交目錄結構和說明,生成快照。
$ git commit -m "first commit"
此外,還有兩個命令也頗有用。
git checkout
命令用於切換到某個快照。
$ git checkout c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
git show
命令用於展現某個快照的全部代碼變更。
$ git show c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
到了這一步,還沒完。若是這時用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/
目錄,該文件的內容就是它所指向的快照的二進制對象名(哈希值)。
下面演示更新分支是怎麼回事。首先,修改一下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
的運行過程是這樣的:
- 查找
HEAD
指針對應的分支,本例是master
- 找到
master
指針指向的快照,本例是785f188674ef3c6ddc5b516307884e1d551f53ca
- 找到父節點(前一個快照)
c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
- 以此類推,顯示當前分支的全部快照
最後,補充一點。前面說過,分支指針是動態的。緣由在於,下面三個命令會自動改寫分支指針。
git commit
:當前分支指針移向新建立的快照。git pull
:當前分支與遠程分支合併後,指針指向新建立的快照。git reset [commit_sha]
:當前分支指針重置爲指定快照。