【轉自 博客專家 zssure https://blog.csdn.net/zssureqh】
面試碰到Git倉庫、緩沖區、工作副本的相關概念,感覺很模糊,故而回來立即查詢相關材料。找到這篇文章感覺寫得不錯,記錄下來。
倉庫、緩沖區、工作副本:
同樣以系列博文GIT科普系列3:底層存儲機制Internal Objects中的整體示意圖為例,介紹這三個概念。

從上圖可以看出工作副本(之所以叫做工作副本是相對於版本倉庫而言的,其實就是你本地的工作目錄)、緩沖區(Stage,或者Index)、倉庫(Commit之后)是三個完全獨立的存儲空間(當然對於GIT管理系統自身而言,這三個區間的內容都以Content-based方式存儲在.git目錄下,詳情參見博文GIT科普系列3:底層存儲機制Internal Objects)。
下面我們就以倉庫、緩沖區、工作副本三個存儲空間為基礎,詳細看一下GIT版本管理的流程,以及常見git指令完成的具體操作。
【備注】:非特殊說明,下文截圖均來自於Visual git guide項目
1. git add

git add指令是日常使用最多的基礎指令(當然如果使用圖形化GIT工具的話,可能會很少用到,都會集成到commit指令中。所以我一再強調最好使用Git Bash來完成相關操作)。以倉庫、緩沖區、工作副本三個存儲空間為出發點來看,git add指令實現的功能就是將工作副本中的非.gitignore排除的files “添加”(備注:這里雖然常常用添加來描述,但是可能最符合語義的應該是“拷貝”,因為git add指令后會在緩沖區存儲空間新增一個二進制BLOB文件,也就是通常認為的快照snapshot)到緩沖區。該指令中支持Bash中常見的通配符,諸如*.jpg、*.txt等,可以快速篩選要添加或過濾不要添加的文件。
2. git commit

git commit是幾乎所有圖形化GIT工具最常用的指令。同樣以三種存儲空間角度出發,git commit就是將緩沖區的內容“添加”到倉庫中,這里的“添加”同樣具有“拷貝”的含義。
上面介紹的git add和git commit指令都是“添加”類指令,也就是如何向GIT的基於內容檢索的文件系統(Content-Basd Filesystem)寫入數據。有寫入就還要有讀取,下面看幾個讀取的指令:
3. git checkout
由於倉庫、緩沖區、工作副本三個存儲空間都是以鏈表的形式來存儲記錄每一次的改動,因此在讀取(上文提到的寫入數據也會跟三個存儲空間具體所在的不同節點有關,但是總體來說數據寫入只要記住一點,都是寫入到當前空間對應的當前節點,所以情況比較簡單)上需要根據三個存儲空間所在的具體節點來進行判別。這也就是git checkout指令復雜的地方,下面分別看幾種checkout指令結果圖:
指令1:
git checkout HEAD~ files #HEAD~代表HEAD的父節點,即~符號表示指針前向移動
- 1

上圖所示此刻實現的功能是將當前HEAD(當前分支處於master,分支名是永遠指向一個分支的最后一個節點)指向的父節點中倉庫的files文件讀取並覆蓋到緩沖區和工作副本,此刻這兩個存儲空間中與倉庫中相同的文件內容保持一致,而往往緩沖區和工作副本兩個存儲空間的內容會比倉庫多,因為很多還未完成的任務沒有提交。
指令2:
git checkout maint #maint代表另一個分支的最后一次提交
- 1

此刻實現的功能是將maint分支最后節點對應的倉庫內的所有文件讀取並覆蓋到緩沖區和工作副本。
指令3:
git checkout master~3#~代表前向移動,3代表移動的節點數
- 1

同樣,此刻實現的功能是將master分支最后節點向前跳躍3個節點對應的倉庫內的所有文件讀取並覆蓋到緩沖區和工作副本。
【備注1】:上述git checkout指令的三種格式都是實現從倉庫“讀取並覆蓋”緩沖區和工作副本。這里要強調一下“讀取並覆蓋”。我們以當前態表示緩沖區和工作副本中的文件、以下一狀態表示所獲取的目標倉庫的內容來詳細講解“讀取並覆蓋”的含義,主要分為幾種:
- 1)當前狀態與下一狀態相同的文件:“讀取並覆蓋”后沒有任何變化
- 2)當前狀態存在但下一狀態不存在的文件:“讀取並覆蓋”后刪除該類文件
- 3)當前狀態不存在當下一狀態存在的文件:“讀取並覆蓋”后最新增該類文件
對於第二種情況要額外小心,因為這會導致你本地未提交處於中間狀態的文件丟失,工作白白忙活了。
【備注2】:上述指令3會出現一種狀態,叫做detached HEAD,言外之意HEAD指針沒有指向任何一個分支(即沒有指向任何一個鏈表的最后節點)。此刻如果提交的話會導致此次提交處於不穩定狀態,如果不進行進一步操作,GIT管理系統會在下一次啟動垃圾回收時刻將該節點刪除。詳情如下:

上圖在detached HEAD狀態下,運行了git commit在git倉庫中創建了一個新的編號為2eecb的節點,關聯到之前checkout后指向的b325c節點,並以此作為父節點。由上圖可以看出,除了HEAD指針指向2eecb節點以外,沒有任何分支指向該節點。這就是所謂的detached HEAD狀態。(為什么只有HEAD指向某個節點時,會被認為是detached HEAD狀態呢?因為HEAD是用來實現多個分支之間跳轉的,即多個鏈表跳轉,而不能為某個鏈表所獨有。只有分支才能屬於某個鏈表,而且都制定分支所在鏈表的最后一個節點)。
此時如果我們執行git checkout master,將HEAD重新移動到master分支時,如下圖所示:

此刻上面新建的2eecb節點就變成了GIT倉庫鏈表的葉子節點,即該節點沒有下一級子節點。對於這種沒有被任何分支或者HEAD所指向的葉子節點,GIT垃圾回收會認為該節點無效,因為從理論上來看你是無法再跳轉到2eecb節點的(當然直接輸入git checkout 2eecb是可以實現這個功能的,誰天天沒事記這個是吧?)。
如果你還是希望保留2eecb這個葉子節點,那么要不你就把HEAD指針移過去(但是此刻你就不能再其它分支進行工作了,因為HEAD要永遠指向當前工作分支),其實這里HEAD指向2eecb時,可以理解為一個“無名分支”或者“匿名分支”。顯然依靠HEAD指針是不合理的,那么只有一種辦法,給2eecb葉子節點建立一個分支,即執行git checkout -b new,如下圖所示即可。

此刻2eecb葉子節點有了new分支指向,就不會被GIT垃圾回收給清理掉了,當然你又多了一個分支。
4. git reset
除了git checkout是讀取類指令以外,還有一個比較很常用的指令,git reset,同樣可以實現“讀取並覆蓋”的功能。
git reset HEAD~3#這個與git checkout的類似,HEAD是當前分支最新節點,~3表示前向移動3個節點
- 1

如上圖所示:git reset相較於git checkout來說更靈活了一些,增加了–soft和–hard兩個參數,可以分別控制“讀取和覆蓋”的工作區間,可同時覆蓋緩沖區和工作副本或只覆蓋緩沖區。
【備注】:仔細觀察git reset與git checkout的兩個讀取指令的示意圖,你會發現git reset強大的地方不單單在於新增了–soft和–hard參數,而是git reset是同時操作分支和HEAD,而git checkout只操作HEAD。——這也就是我們之前提到了GIT實現回滾的底層依據,詳情參見博文GIT科普系列1:git如何放棄本地working directory的修改,以及回滾。
總結:
以倉庫、緩沖區、工作副本三個獨立存儲空間為基礎,就比較容易理解git的版本管理機制,以及各個指令所實現的功能。在此基礎上,再熟背git指令,你就可以自由的、輕松的來管理的文檔了。fighting!
作者:zssure@163.com
時間:2016-09-17