git reset 命令詳解(一)—— Git 學習筆記 07

ARM的程式設計師敲著詩歌的夢發表於2018-08-19

git reset 命令詳解(一)

簡而言之,git reset 命令是用來將當前 branch 重置到另外一個 commit 的,這個動作可能同時影響到 index 以及 work directory.

先舉個例子,來一個感性的認識。下面這兩條命令讓 hotfix 分支向後回退兩個提交。

git checkout hotfix
git reset HEAD~2

這裡寫圖片描述

hotfix 分支末端的兩個提交現在變成了孤兒提交。下次 Git 執行垃圾回收的時候,這兩個提交會被刪除。如果你的提交還沒有共享給別人,可以用git reset撤銷這些提交。

三棵樹

理解 reset (以後還要說 checkout )的最簡方法,就是以 Git 的思維框架(將其作為內容管理器)來管理三棵不同的樹。“樹” 在我們這裡的實際意思是 “檔案集合”,而不是指特定的資料結構。

Git 作為一個系統,是以它的一般操作來管理並操縱這三棵樹的:

用途
HEAD 上一次提交的快照,下一次提交的父結點
Index 預期的下一次提交的快照
Working Directory 沙盒

HEAD 是當前分支引用的指標,它總是指向該分支上的最後一次提交。 這表示 HEAD 將是下一次提交的父結點。 通常,可以把 HEAD 看做你的上一次提交的快照。

下面是我從網上搜來的 2 張圖片,感謝原作者。

這裡寫圖片描述

這裡寫圖片描述

大意就是:HEAD 指向分支(branch),分支指向提交。

索引(index)

索引是你預期的下一次提交。這就是當你執行 git commit 時 Git 看起來的樣子。Git 將上一次檢出到工作目錄中的所有檔案填充到索引(暫存區),之後你會將其中一些檔案替換為新版本,接著通過 git commit 將它們轉換為樹來用作新提交。

工作目錄

另外兩棵樹以一種高效但並不直觀的方式,將它們的內容儲存在 .git 資料夾中。工作目錄會將它們解包為實際的檔案以便編輯。 你可以把工作目錄當做 “沙盒”,在你將修改提交到暫存區並記錄到歷史之前,可以隨意更改。

流程圖解

下文會用一幅幅圖說明從初始化倉庫到操作工作區,再到 add 檔案,最後到提交等整個流程。

僅工作目錄有內容

假設我們進入到一個新目錄,其中有一個檔案。 我們稱其為該檔案的 v1 版本,將它標記為藍色。 現在執行 git init,這會建立一個 Git 倉庫,其中的 HEAD 引用指向未建立的分支(master 還不存在)。

這裡寫圖片描述

新增到索引

現在我們想要提交這個檔案(file.txt ),所以用 git add 把工作目錄中的內容複製到索引中。

這裡寫圖片描述

提交

接著執行 git commit,它會取得索引中的內容並將它儲存為一個永久的快照,然後建立一個指向該快照的提交物件,最後更新 master 來指向本次提交。

這裡寫圖片描述

此時如果我們執行 git status,會發現沒有任何改動,因為現在三棵樹完全相同。

修改檔案

現在我們想要對檔案進行修改然後提交它。我們將會經歷同樣的過程;首先在工作目錄中修改檔案。 我們稱其為該檔案的 v2 版本,並將它標記為紅色。

這裡寫圖片描述

如果現在執行 git status,我們會看到檔案顯示在 “Changes not staged for commit” 下面,並被標記為紅色,因為該條目在索引與工作目錄之間存在不同。 接著我們執行 git add 來將它暫存到索引中。

再次新增到索引

這裡寫圖片描述

此時,由於 Index 和 HEAD 不同,若執行 git status 的話就會看到 “Changes to be committed” 下的該檔案變為綠色 ——也就是說,現在預期的下一次提交與上一次提交不同。 最後,我們執行 git commit 來完成提交。

再次提交

這裡寫圖片描述

現在執行 git status 會沒有輸出,因為三棵樹又變得相同了。

切換分支或克隆的過程也類似。 當檢出一個分支時,它會修改 HEAD 指向新的分支引用,將索引填充為該次提交的快照,然後將索引的內容複製到工作目錄 中。

reset 的作用

在以下情景中觀察 reset 命令會更有意義。

為了演示這些例子,假設我們再次修改了 file.txt 檔案並第三次提交它。 現在的歷史看起來是這樣的:

這裡寫圖片描述

現在,假設我們執行git reset HEAD~(後面可能會跟不同的引數)。

第 1 步:移動 HEAD

reset 做的第一件事是移動 HEAD 的指向。 這與改變 HEAD 自身不同(checkout 所做的);reset 移動 HEAD 指向的分支。 這意味著如果 HEAD 設定為 master 分支(例如,你正在 master 分支上),執行 git reset 9e5e6a4 將會使 master 指向 9e5e6a4。

這裡寫圖片描述

使用 reset –soft,它將僅僅停在那兒。

結合上圖,我們理解一下發生的事情:它本質上是撤銷了上一次 git commit 命令。 當你在執行 git commit 時,Git 會建立一個新的提交,並移動 HEAD 所指向的分支來使其指向該提交。 當你將它 reset 回 HEAD~(HEAD 的父結點)時,其實就是把該分支移回原來的位置,而不會改變索引和工作目錄。

第 2 步:更新索引(–mixed)

接下來,reset 會用 HEAD 指向的當前快照的內容來更新索引。

這裡寫圖片描述
如果指定 –mixed 選項,reset 將會在這裡停止。 這也是預設行為,即如果沒有指定任何選項(在本例中是 git reset HEAD~),reset 將會在這裡停止。

現在再看一眼上圖,理解一下發生的事情:它依然會撤銷一上次提交,但還會取消所有暫存。 於是,我們回滾到了所有 git add 和 git commit 的命令執行之前。

第 3 步:更新工作目錄(–hard)

如果使用 –hard 選項,reset 要做的的第三件事情就是讓工作目錄看起來像索引。

這裡寫圖片描述

現在讓我們回想一下剛才發生的事情:你撤銷了最後的提交(git commit )、git add 和工作目錄中的所有工作。

必須注意,–hard 標記是 reset 命令唯一的危險用法,它也是 Git 會真正地銷燬資料的僅有的幾個操作之一。其他任何形式的 reset 呼叫都可以輕鬆撤消,但是 –hard 選項不能,因為它強制覆蓋了工作目錄中的檔案。

總結

reset 命令會以特定的順序重寫這三棵樹,在你指定以下選項時停止:

  1. 移動 HEAD 指向的分支 (若指定了 --soft,則到此停止);
  2. 重置 index 以便和 HEAD 相匹配 (若未指定 --hard,則到此停止);
  3. 使工作目錄看起來像索引




參考資料

【0】《Pro Git》(Scott Chacon, Ben Straub Version 2.1.14, 2018-05-19)

【1】https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting

相關文章