關於Git暫存區的理解
暫存區可以說是Git的三大重要的區域之一,另外兩個分別是工作目錄和Git倉庫,所以說對暫存區的深入理解可以幫助我們理解很多Git命令背后隱藏的工作原理。今天,本文將以一名初學者的角度詳細講解Git暫存區,涉及到的Git命令也會作簡單的介紹但不會深入。
本文鏈接:
http://www.cnblogs.com/cposture/p/4178003.html
在這里,我們先拋出Git暫存區的概念和相關理解,后面會一一說明。
(1)所謂的暫存區只是一個簡單的索引文件而已。
(2)暫存區這個索引文件里面包含的是文件的目錄樹,像一個虛擬的工作區,在這個虛擬工作區的目錄樹中,記錄了文件名、文件的時間戳、文件長度、文件類型以及最重要的SHA-1值,文件的內容並沒有存儲在其中,所以說 它
像一個虛擬
的工作區。
(3)索引指向的是.Git/objects下的文件。
(4)暫存區的作用:
除非是繞過暫存區直接提交,否則
Git想把修改提交上去,就必須將修改存入暫存區最后才能commit。每次提交的是暫存區所對應的文件快照。
好了結論說到這,上面的結論開始接觸時你不一定都能理解,下面開始進行詳細說明。
當我們在某個目錄下運行git init命令后,在該目錄下便會生成一個.git的子目錄,這個目錄是隱藏的。它是用來保存元數據以及對象數據庫的地方,這個目錄可以說是Git的核心,每次克隆鏡像倉庫時,實際上拷貝的這個目錄里的內容而已,然后再根據這個目錄恢復工作目錄(工作目錄:你所git init的目錄,但不包含.git這個隱藏的目錄),即在工作目錄下生成相應的項目文件和目錄。而我們的暫存區也位於這個.git目錄之下,名為index,它一般很小,一般不超過1KB左右,這和它存儲的內容是有關系的。
在這個目錄下,除了index這個暫存區索引文件,還有其他重要的文件和目錄,其中有存放數據對象的objects目錄(這可以說是我們Git的一個倉庫了)、存放引用文件的refs目錄以及存放指向當前分支的HEAD文件。
和暫存區極其相關的一條命令是
-
git add <文件名>//將一個文件添加進暫存區
git add可以分兩條底層命令實現:
1 git hash-object <文件名> 2 git update-index <文件名>
運行第一條命令,Git將會根據新生成的文件產生一個長度為40的SHA-1哈希字符串,並
在.git/objects目錄下生成一個以該SHA-1的前兩個字符命名的子目錄,然后在該子目錄下,存儲剛剛生成的一個新文件,新文件名稱是SHA-1的剩下的38個字符。
第二條命令將會更新.git/index索引,使它指向新生成的objects目錄下的文件。
在objects目錄下新生成的文件是什么文件呢?
在這個目錄下,Git將其下文件分為四種類型,其中有兩種是tree類型和blob類型,blob類型文件存儲的就是我們運行命令增加的文件加上一個特定的文件頭,而tree類型文件就相當於我們系統下的目錄了,里面存儲的是多條記錄,
每一條記錄含有一個指向 blob 或子 tree 對象的 SHA-1 指針,並附有該對象的權限模式 (mode)、類型和文件名信息,它和blob類型對象不一樣,存儲的並非文件的內容。
如果生成一個新的文件存入Git倉庫中而沒有第二步的更新.git/index索引,那么將會發生兩種情況:
(1)如果該文件從未添加進暫存區,也就是將該文件從未更新到index索引中,那么Git是會一直顯示未跟蹤的文件。
模擬一下該情況,可以選擇一個無用的目錄進行git init,然后在運行git status,在目錄下新建一個test.txt文件並增加內容,運行git status,運行git hash-object d.txt,再運行git status。
1 #初始化倉庫 2 $git init 3 Initialized empty Git repository in E:/t/test/.git/ 4 #查看Git當前狀態 5 $git status 6 On branch master 7 Initial commit 8 nothing to commit (create/copy files and use "git add" to track) 9 #在當前目錄下新建一個內容為"this is a test file"的test.txt 10 $echo 'this is a test file ,version 1'> test.txt 11 #再次查看Git的狀態,Git會提示工作目錄下有一個未跟蹤的文件 12 $git status 13 On branch master 14 Initial commit 15 Untracked files: 16 (use "git add <file>..." to include in what will be committed) 17 test.txt 18 nothing added to commit but untracked files present (use "git add" to track) 19 #此時查看.git/objects目錄,只有兩個文件夾info和pack 20 $git hash-object -w test.txt 21 4617a36485976a90eb72e7020911dea0c892956b 22 #此時.git/objects目錄下有了46子目錄以及該子目錄下17a36485976a90eb72e7020911dea0c892956b文件 23 #Git仍然顯示test.txt為未跟蹤文件 24 $git status 25 On branch master 26 Initial commit 27 Untracked files: 28 (use "git add <file>..." to include in what will be committed) 29 test.txt 30 nothing added to commit but untracked files present (use "git add" to track)
(2)如果文件曾經添加進暫存區,那么結果會是怎樣?那么結果顯示的是修改結果未放入暫存區。
模擬下情況:git add,git status ,git commit -m "add test.txt file ,version 1",git status,修改文件,git status
1 #將我們上次的test.txt添加進暫存區 2 $git add test.txt 3 #查看狀態 4 $git status 5 On branch master 6 Initial commit 7 Changes to be committed: 8 (use "git rm --cached <file>..." to unstage) 9 new file: test.txt 10 #提交 11 git commit -m "add test.txt file ,version 1" 12 #查看當前狀態,當前工作目錄是干凈的,沒有什么要提交的了 13 #此時查看.git/objects目錄,多了兩個子目錄,一個是tree對象,一個是commit對象暫時忽略它 14 $git status 15 On branch master 16 nothing to commit, working directory clean 17 #繼續修改 18 $echo 'this is a test file ,version 2'> test.txt 19 #Git顯示有一個修改文件未放入暫存區 20 On branch master 21 Changes not staged for commit: 22 (use "git add <file>..." to update what will be committed) 23 (use "git checkout -- <file>..." to discard changes in working directory) 24 modified: test.txt 25 no changes added to commit (use "git add" and/or "git commit -a") 26 $git hash-object -w test.txt 27 1ed62f7cc06e6a57cdbaf9a5a5023aa93bd1ffb1 28 $git status 29 On branch master 30 Changes not staged for commit: 31 (use "git add <file>..." to update what will be committed) 32 (use "git checkout -- <file>..." to discard changes in working directory) 33 modified: test.txt 34 no changes added to commit (use "git add" and/or "git commit -a")
前后兩次,一次顯示未跟蹤,一次顯示未放入暫存區,我們可以得出一個結論:即使在.git的倉庫中生成了相應的數據對象文件,如果沒有update index更新索引,索引還是指向.git/objects下舊版本的文件或者文件在index中沒
根本
有索引,Git也無法commit。
如果我們在第二種情況下,更新索引同時提交,那么提交的test.txt版本是最近的一次add的版本。另外,我們可以看到git add 后更新的暫存區只是一些文件索引,而文件的快照是保存在.git/objects目錄下。
如果你在工作目錄下再新建一個文件,查看Git狀態,Git只顯示剛剛新建的文件為未跟蹤文件,而Git對我們那個test.txt沒有提示,這時我們add新建的文件然后提交,提交的文件包括了剛剛新建的文件和test.txt,我們每一次提交時,都無需對曾經git add 的文件再進行一次git add 操作,說明每次add后,Git都會在index下更新文件索引,而且該索引是一直存在的,除非使用git rm刪除。
1 #創建一個new.txt新文件 2 $echo 'this is a new.txt file'> new.txt 3 #添加到暫存區並提交,此時可以看到提示信息中有master b5e4b5c,該數值為commit對象的SHA-1值 4 $git add new.txt 5 $git commit -m "add new.txt file" 6 [master b5e4b5c] add new.txt file 7 1 file changed,1 file insertion(+) 8 create mode 100644 new.txt 9 #通過cat-file命令可以查看對象中存儲的內容,可以看到master commit對象指向的是一棵樹對象,即代表根目錄 10 $git cat-file -p b5e4b5c 11 tree 019ab86ed8f73265378a25dc00bff6c085f35cb0 12 parent 8d50d0c061da5104551bdbaa060e0d4c916411c1 13 author cposture <cposture@126.com>1418539116+0800 14 committer cposture <cposture@126.com>1418539116+0800 15 add new.txt file 16 #查看樹對象根目錄的存儲內容,可以發現tree對象存儲的是指向兩個我們創建的文件SHA-1值和其他相關信息,而不是數據本省內容 17 $git cat-file -p 019ab86 18 100644 blob 3e1267f7b311e69a691cf2ff5b90b46afaa1a359 new.txt 19 100644 blob d5210ccb24bd6e8bbc994ade3be3b48a9001f3b7 test.txt
理解暫存區,還要弄清楚我們到底提交上去的是什么?這和暫存區有什么聯系?
回答這個問題前,我們先回想一下,上文說的.git/objects下文件有四種類型,其中第三種是commit對象。
commit對象
指明了該時間點項目快照的頂層樹對象、作者/提交者信息(從 Git的
user.name
和user.email
中獲得)以及當前時間戳、一個空行,以及提交注釋信息。
簡單地增添一個文件后,
我們每次執行git commit操作,一般都會在.git/objects目錄下生成另外兩個文件(這在上文模擬的第二種情況的命令行的第13行注釋中也提到過),一個是tree對象類型文件,另一個便是commit類型文件,為什么會生成兩個文件而不是一個。
這個commit對象中包含的tree對象SHA-1值指向了最新版本的根目錄樹對象。這個tree對象是根據你的暫存區而創建的,每個tree都是這樣,所以創建tree前都需將文件或目錄添加進暫存區。如果是添加的是目錄則會再生成一個子tree對象,再根據目錄下的文件分別生成相應的blob普通文件和子tree對象。這個根tree對象中包含的多條記錄,簡單的概括,分別是修改后添加進暫存區SHA-1改變了的文件或目錄(上文說過的git add 分為的兩步,其中最后一步是更新修改文件在暫存區的SHA-1值)或者是沒修改但曾添加進暫存區,SHA-1值仍不變的文件或目錄。Git正是拷貝暫存區的這些目錄樹和文件的SHA-1等信息到commit對象而生成Git的一個提交,即commit對象,commit對象保存的提交完整的記錄了當前所有已跟蹤文件的快照。所以你所提交的項目來自於暫存區!

簡而言之,文件索引即暫存區,建立了文件和.git/objects目錄下的對象實體之間的映射關系,如下圖:
