圖文詳解 Git 的使用場景

無論學習什麼技術,都需要了解該技術的本質。若是靠死記硬背該技術提供的方法或者語法,終歸是知其然而不知其所以然,當發現錯誤時,你根本不知道是什麼原因導致的。我在使用Git時,就處於這種知其然而不知其所以然的狀態。現在,再來補補課。

Git有三個工作區域,分別爲:工作目錄(Working Directory)、暫存區(Stage或Index)以及資源庫(Repository或Git Directory)。下圖是文件在這三個工作區域之間的關係:

參考Pro Git一書,它給出了Git的幾個要點: * 直接快照,而非比較差異:Git與其他版本管理系統的主要差別在於,Git只關心文件數據的整體是否發生了變化,而其他多數版本管理系統則只關心文件內容的具體差異。Git並不保存文件前後變化的差異數據,更像是把變化的文件做一個快照,然後記錄在一個微型的文件系統中。每次提交更新時,會比較這個快照。若文件沒有變化,Git則只對上次保存的快照作一個鏈接。你可以理解Git就是一個小型的文件系統。 * 近乎所有操作都可本地執行:無需多說,這本身就是分佈式版本管理系統的特徵。 * 時刻保持數據完整性:保存到Git前,所有數據都要進行內容的校驗和(checksum),並將該結果作爲數據的唯一標識。Git使用了SHA-1算法計算數據的校驗和,並將該結果作爲索引,而非文件名。

* 多數操作僅添加數據

Pro Git一書認爲任何一個文件在Git內部可以被分爲三種狀態:已提交(Committed)、已修改(Modified)和已暫存(Staged)。然而,這並不足以說明一個文件在不同的工作區域所展現的狀態。我認爲兩種狀態足以表達Git中的文件,即:未跟蹤(Untracked)和已跟蹤(Tracked)。而對於已跟蹤狀態,我又將其分爲:未修改的(Unmodified),Modified(已修改的),暫存的(Staged)和已提交的(committed)。下圖基本表達了我的思路:

這個圖表現了多種場景,滿足了我們在使用Git時耳濡目染的操作情形。

場景1:暫存文件以及取消已暫存的文件

可以參考上圖中上面部分黑色箭頭標示。當我們通過git init在本地初始化了Git工作目錄後,新增了一個README.txt文件時,此時該文件處於Untracked狀態。接下來執行命令:

  1. git add README.txt 

add命令可以暫存此文件,此時,狀態變更爲Staged狀態,被放到了Git暫存區中。若我們要提交此文件到Git資源庫,就可以執行git commit命令,文件狀態變爲committed。例如:

  1. git commit -m "first commit" 

有時候,我們希望取消已暫存的文件。例如說,我在工作目錄中增加了兩個文件,然後暫存了它們。後來發現其中一個文件並不需要在Git中管理,希望能夠取消暫存。由於此時的文件處於Staged狀態,我們只需要刪掉Stage中對此文件的跟蹤即可。這時需要執行的命令是:

  1. git rm --cached README.txt 

注意:此時取消暫存的文件從來就不曾提交過,也即是說沒有在Git Repository留下過它的身影。這時的取消暫存實則是刪掉暫存的信息。與後面場景演示的取消暫存並不相同。

場景2:修改已提交文件以及取消已暫存的內容

一旦文件被提交,就會在Git Repository形成提交記錄(以hash作爲鍵)。倘若我們此時push提交到遠程Git服務器,Git服務器應與本地庫保持一致。

現在,讓我們看看圖中紅色箭頭展現的流程。我們修改了已提交的README.txt文件,於是文件狀態就變更爲Modified。這部分修改的內容並沒有被放入暫存區,若要提交此次修改,就還需要再次執行git add命令,將這次新的修改放入到暫存區。這個流程包括後面的提交都與場景1相似。唯一不同的是“取消已暫存的內容”。

雖然同樣是取消暫存,但它與場景1是完全不同的概念。場景1實則是要取消暫存區的文件,因此使用了git rm –cached,本質上講其實是刪除。而這裏的取消,其實是希望取消暫存區中已經被添加的修改內容,文件本身仍然保留在暫存區中。故而執行的命令爲:

  1. git reset HEAD README.txt 

HEAD是何意呢?在Git中,HEAD是一個特別的指針,指向你正在工作的本地分支。當前分支就是master。如下圖所示:

而reset命令的意思是重新設置當前的HEAD指針到特定的狀態。由於當前的README.txt還沒有提交到master分支的Repository中。因此,這條命令實則就是將HEAD指向README.txt文件在當前master分支的Repository狀態,從而保證了對README.txt文件而言,暫存區與Repository的一致——取消了README.txt文件在暫存區的內容。

場景3:修改文件以及撤銷修改內容

再看圖中的綠色箭頭與藍色箭頭展現的流程。我們不是初始化git工作目錄,而是通過git clone從遠程Repository克隆了項目,此時會在當前目錄建立git工作目錄。此時的文件全部處於Unmodified狀態。

現在,我們修改文件,例如hello.java。一旦被修改,文件狀態就遷移到Modified狀態。倘若需要暫存此次修改,甚至提交到Git Repository,則執行的流程與場景1相同(如藍色箭頭線所示)。

然而,我們可能希望放棄此次修改,即不將修改的內容放入暫存區。這時,應執行checkout命令:

  1. git checkout -- hello.java 

在執行checkout命令時要慎重。因爲它要撤銷的內容並沒有被放入到暫存區或Repository。一旦撤銷,就一去不復返了。

概念區分:fetch vs. pull

fetch命令只是將遠端數據拉到本地倉庫,並不自動合併到當前工作分支。若要合併,還需手動合併。例如,執行git fetch origin,就會抓取自上次克隆以來別人上傳到此遠程倉庫中的所有更新。

pull命令則除了會抓取數據,還能將遠端分支自動合併到本地倉庫中當前分支。

場景4:撤銷提交

在Git中若要撤銷提交,可以使用reset或者revert命令。但二者有着顯著的區別:

revert命令可以撤銷已經提交的快照,但它並不會將該提交從項目的提交歷史中移除,而是會判斷要撤銷的這次提交引入了哪些變化,然後將此變化撤銷(此次撤銷事實上還是一種變化),再將這次撤銷作爲一個提交。因此,在執行revert命令後,如果通過git log查看提交歷史,可以看到會新增一個revert提交。命令爲:

  1. git revert <commit> 

這個commit可以是指定提交對應的hash code。我們也可以用HEAD指針:

  1. git revert HEAD~n 

如果是revert當前提交,則不需要HEAD後的~n。

reset命令就字面意義已經表達了該操作的含義爲“重置”。由於Git的提交記錄是由HEAD指針指向當前分支。重置就是搬動這個指針到指定的snapshot。如果說revert是一種 安全的撤銷方式,則reset就是一種 危險的撤銷方式。默認情況下,如果使用reset命令,會將當前的分支回退到指定commit,然後自指定commit到最新commit之間的內容會放在工作目錄下,使得我們可以再提交。這個命令爲:

  1. git reset <commit> 

與前相同,這個commit就是提交對應的hash code。同樣,也可以使用HEAD指針。不過如果是撤銷當前提交,與revert不同的是,需要指定爲:HEAD~1。這是因爲HEAD指針指向了當前提交。reset與revert的意義不一樣。revert對應的commit爲目標提交,意思爲:“撤銷目標提交”,因而git revert HEAD,代表的就是“將當前提交撤銷”。而reset對應的commit表示將指針移向給定的Commit。如果執行git reset HEAD,代表的就是“將當前指針指向當前提交”,相當於沒做任何操作。所以應該執行git reset HEAD~1。

如果確實要撤銷操作,而前面的內容並不需要,在使用reset命令時,可以添加–hard參數:

  1. git reset --hard <commit> 

**注意:針對遠程的提交記錄,應儘量避免使用git reset命令。倘若在本地進行了reset之後,又進行了另外的修改並提交。此時,本地的提交記錄與遠程的提交記錄在reset的那個點產生了分叉。如下圖所示: 

此時,如果執行git push,會在本地合併後提交,並同步遠程提交記錄。則團隊其他成員會因爲這個變化的提交記錄而困惑。由於一部分變更消失,甚至可能導致一些數據被破壞。因此,使用reset命令要格外當心,通常情況,應儘量針對本地提交(未push到遠程)進行reset。優先考慮使用revert命令。

發佈了5 篇原創文章 · 獲贊 5 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章