# 前言
git reset
,一個之前文章時不時會出現的字詞,但當時文章又一直不認真說明。
看看這篇文章右手邊的滾軸有多小,你大概就能體諒我為什麼當初對 reset 一直要說不說的了 Orz…
在開始介紹 reset 用途之前,有幾個先備知識複習一下,對後面的內容理解可能會有幫助。因為觀念很重要,後續還是會繼續提及。
之前把 「暫存區 (索引)」比喻成「購物車」,接著我們把 「提交版本」 比喻成 「商品下單」,這可能讓讀者以為「提交版本」之後,「索引」就會被清空。
不過在 Git 中,提交版本的行為「不會」清空索引裡面記錄的資料!
提交版本 更貼切的想像,應該是把 commit
想成一個紙箱,即便紙箱中「有東西」,我們也看不到裡面的東西。
所以提交版本 ( git commit
) 的行為,比較像是拿紙箱把商品打包那樣,把「暫存區的資料」封裝起來,讓我們在 「暫存區」看不到資料。
這些被打包的資料依舊存在暫存區中,只是我們不用在意他,但如果有人把這個箱子拆掉,我們將會重新看到這些資料。
會需要有這層先備知識的原因是, git reset
就是一個能對 commit 與 暫存區 動手腳的指令。
整篇文章的內容,我們會持續使用這個觀念,有了這個觀念之後,可能也比較能夠體會 reset 究竟是在玩什麼把戲!
由於 git reset
需要搭配實際情境會比較好理解,所以觀念跟指令會夾雜在一起說明,整個講解完之後,會再帶到 Fork GUI 的操作方式。
請 GUI 派的讀者別擔心, reset
的指令本身並不複雜,只要先搞懂 reset
是怎麼處理資料,在 GUI 中就可以輕鬆操作 reset
了!
話不多說,進入今天的主題:
# git reset
git reset
在 Git 中最主要的用途,是將 HEAD
移動到其他 commit 上,以達到「回到過去」的作用。
聽起來可能有點難懂,讓我一步一步來說明 reset
的用意。
reset
照著字面翻譯成中文,可以叫做 重新設定 ,或簡稱 重置,代表著他是用來幫我們把時間重置到某些動作「之前」的狀態。
在操作 Git 時,一般會經歷下面幾個步驟:
- 編輯好一些檔案。
- 執行
git add
把這些檔案加到暫存區。 - 執行
git commit
提交一個版本。 - Git 線圖會出現剛剛 commit 的版本點點。
假設今天這個動作執行了五次,也就是讓 Git 紀錄五個版本。
就叫 A
=> B
=> C
=> D
=> E
好了,所以此時的 HEAD
指著版本 E
。
使用 reset
指令可以讓 HEAD
倒退回去 A
、 B
、 C
、 D
任何一版。
如果移回 D
,就像搭著時光機回到了 D
的時間點,所以 Git 會把版本 E
從 Git 線圖 中拆掉。
舉一反三,退回 D
會拿掉 E
的話…
那麼退回 B
呢?
沒錯! C
、 D
、 E
三個 commit 箱子都會被拆掉。
這感覺有點像疊疊樂積木,當積木疊到最高點的時候, HEAD
會在最上層的積木堆中。
而把 HEAD 往前移的行為,就代表著要把上面的積木拿掉才行。
(再加上箱子的概念… 那應該會變成箱子疊疊樂?!)
在知道 commit 會因為 reset 而被拆掉之後,下一個要擔心的事情應該會是:
資料呢?原本紀錄在 commit 的那些資料會不見嗎…?
可能會,也可能不會。
容我賣個關子,先讓我來介紹 git reset
的語法,至於資料何去何從,後續會慢慢揭曉。
# git reset 語法
git reset commitID 模式 |
或是
git reset 模式 commitID |
這個指令中有兩個參數是可變的:
-
commitID
commitID
的預設值是HEAD
,如果要 reset 回HEAD
,這個值可以省略。
如同之前 聊聊 commitID 與 HEAD 文章所說明的觀念, commitID 可以用「哈希值」,也可以使用 「HEAD 操作相對位置」。 -
模式
reset
指令有許多模式能使用,預設模式是 mixed,一樣如果想使用--mixed
模式,這個值可以省略不寫。
綜合上述的預設值來說,如果我們只寫 git reset
時,實際會執行這個指令:
git reset --mixed HEAD |
# git reset 常見三種使用模式
我們使用 reset 回到過去的當下,因為有些 commit 箱子被拆了,所以 Git 手上會拎著原本被這些 commit 包裝的那些資料,Git 需要知道這些內容該被還原,還是直接丟掉。
就像上面講的積木那樣,把上頭積木一層一層拿掉,拎在手中的積木何去何從,會是個大問題。
To be or not to be. That is a question. - 莎士比亞《哈姆雷特》
為了處理資料的去留,Git 提供幾種參數讓我們操作,其中最常見的有三種: --mixed
、 --soft
、 --hard
。
可以用 git reset -h
指令來看看 reset 的參數說明:
(這裡只擷取常用三種模式)
$ git reset -h | |
--mixed reset HEAD and index | |
--soft reset only HEAD | |
--hard reset HEAD, index and working tree |
可以注意到,不管哪一種模式,都會重置 (reset) HEAD,其實就是在講上面那個積木的概念, reset
會移動 HEAD
指向 commit 的意思。
除了一定會做的「移動 HEAD」 之外,依據不同的模式,可能還會有重置索引 (index),以及重置工作目錄 (working tree) 的動作。
接著就來給大家翻譯翻譯,什麼叫做 驚喜 重置索引 與 重置工作目錄。
# git reset --mixed
如同在介紹語法的時候說的,這是預設模式,要使用的話參數能省略。
根據定義, --mixed
模式會重置 HEAD 以及 index:
- 重置 HEAD:表示要上 HEAD 移動到特定 commit,這大家應該都知道了。
- 重置 index:意思就是就是把資料從索引中清除。
根據前言的內容,commit 箱子被拆掉之後,資料其實還是留在暫存區的。
預設模式除了把箱子拆掉之外,他額外還會把資料「移出暫存區」。
這就會使資料呈現一個「剛編輯完」的狀態。
(對吧?!剛編輯完的資料,是沒加入暫存區 (購物車、索引) 的狀態,所以跟被移出暫存區的資料是一樣的沒錯吧?!)
要強調的是, --mixed
模式 「不會異動」 工作目錄的資料!
就是說執行完 reset 預設模式之後,工作目錄的檔案 完全不會 被改變!
以實際的例子來看看到底發生了什麼事情:
目前 Git 記錄著兩個 commit ,一個是 initial commit ,另一個是 add style.css。
第二個 commit 中有兩個檔案,分別是 readme.txt
與 style.css
:
執行 git reset HEAD^
之後, git status
會這麼顯示:
紅字的內容就是原本 commit 所記錄的資料,由於少了 commit 當載體,又被移出了索引 (怎麼聽起來很可憐),於是成了 未暫存 的狀態了。
# git reset --soft
根據定義,soft 模式只會重置 HEAD。
這個模式就很好理解了,使用 --soft
模式把 HEAD 往前移動之後,多出來的 commit 就只會被拆掉,但資料繼續留在暫存區 (索引)。
我們繼續用剛剛的例子執行 git reset HEAD^ --soft
之後來看狀態吧:
用 soft 模式執行 reset ,雖然資料也是少了 commit 裝箱,不過 soft 不會去異動索引,所以 git status
會告訴我們「欸!你還有資料在暫存區,是可以 commit 的哦」。
這個情況就很像「已經執行 git add .
,但還沒執行 git commit
」 的時間點了。
一樣也是強調一下,「柔軟模式」因為只移動 HEAD,所以他也不會改變工作目錄的檔案。
# git reset --hard
根據定義,hard 模式同時會重置 HEAD 、index 還有 Working Tree。
這個模式我們就不要講拆箱子或是索引有沒有資料了,最簡單的理解方式是這樣:
使用 hard 模式之後,資料會 完整呈現 指令寫的那個 commit 資料,不會有絲毫差異。
如果你看不懂什麼叫「指令寫的那個 commit」,就是在說這個:
git reset --hard commitID # <= 就是在說這個 commit |
言下之意,–hard 模式就是要把工作目錄狀態直接還原到這個 commit 剛提交完的狀態。
所以不會有任何資料還是 暫存 或是 未暫存 的狀態。
這時候執行 git status
就是會直接告訴你 nothing to commit 。
實際來看一下,圖中的第一個 commit 只有 readme.txt
一個檔案,之後的 commit 有什麼東西不重要。 反正等等就看不到了
執行 git reset commiID --hard
後 (註 1) ,工作目錄中就只會出現 reset 過去 commit 的資料。
註 1. reset 指令除了操作 HEAD 之外,也可以直接使用 commitID 來指定要重置的 commit,這裡刻意示範使用 commitID 的指令,這個情境因為是退回前一個版本,所以也可以使用
git reset HEAD^ --hard
做到一樣的效果。
# git reset 模式整理
用一個表格來整理操作 reset
時,那些 commit 箱子被拆掉之後,工作目錄與索引會處理資料的方式。
模式 | mixed (預設) | soft | hard |
---|---|---|---|
工作目錄 | 保留 | 保留 | 捨棄 |
索引 (暫存區) | 捨棄 | 保留 | 捨棄 |
一下索引一下工作目錄的,可能已經有人看得頭都暈了。
讓我再換種方式說明,這次試著用「回溯動作」的方式來看看三種模式的差異在哪。
假設我們的操作是:
- 提交 (commit) 版本
A
- 編輯好某些檔案的資料
- 將資料加入暫存區
- 提交 (commit) 版本
B
<= 此時 HEAD 指著他
從版本 B
以不同的模式 reset 回版本 A
,剛好會對應到不同的情境
- soft 模式:回到第 3 步,剛把資料加入暫存區的狀態。
--mixed
模式:回到第 2 步,剛編輯完某些檔案的狀態。- hard 模式:回到第 1 步,剛提交完版本 A 的狀態。
# git reset 模式使用情境
說了很多模式,但如果不搭配情境,或許還是有人想像不到 reset
能幹嘛。
這裡簡單說幾個 reset
可能會用到的情境。
# 1. 移除最新的 commit
記得昨天的 –amend 文章就有提到,要移除最近一次 commit 其中一個檔案,使用 reset
反而比較方便。
由於直接使用 reset
預設模式,不會影響到工作目錄的資料,他又能幫我們將資料移出暫存區,如同幫我們把時光回溯到檔案編輯完的狀態。
言下之意,執行完這個模式的 reset 之後,我們可以重新決定 commit 要提交哪些檔案,甚至資料的內容、提交的說明都可以重新來過。
而這個彈性很大的指令,就長這樣:
git reset HEAD^ |
拆掉 commit 箱子,讓 HEAD 回到上一版。
# 2. 想捨棄所有修改的資料
前天的文章有提到,我們可以用 git restore .
或是 git checkout -- .
捨棄所有未暫存的資料。
不過這個需求使用 hard reset ,可能會更直覺。
git reset HEAD --hard # HEAD 可以省略不寫 |
因為 --hard
模式可以直接還原到某個 commit 「剛提交完」的狀態,指令執行完之後,不會有任何資料會是 暫存 或是 未暫存。
剛好 reset
預設的位置就是 HAED
,執行上面指令代表位置不動,但直接幫我 捨棄一切 異動,如此一來就達成捨棄所有修改資料的目的了。
# 3. 想在最近一個版本中加一個檔案
這需求用上一篇文章的 --amend
就可以處理。
如果想用 reset
處理的話,除了使用 --mixed
模式,也可以使用 soft 模式。
不過如果只是要追加檔案,用 soft 模式可能會方便一些。
git reset HEAD^ --soft |
聰明如你可能會說:「有差嗎?就算使用 --mixed
模式,我直接執行 git add .
就好啦!」
那麼假設,你的工作目錄的資料,還有很多剛新增的「未追蹤」(Untracked) 的檔案,是短時間還沒打算 commit 的呢?
如果直接 git add .
,這堆沒有要 commit 的資料,也會一次加到暫存區哦…
在這種狀況下使用 --mixed
模式,可能就需要一筆一筆執行 git add
把想 commit 的檔案加到暫存區。
不過如果使用 soft 模式,因為不會把原本的資料移出暫存區,就只需要用 git add
某 一個 要追加的檔案,就結束了。
既然資料最後都還是要加回暫存區,就別大費周章去用 mixed 先移除又加入了吧!
# 使用 Fork GUI 來操作 reset
來介紹一下怎麼使用 Fork 操作 reset 的動作。
-
對著想
reset
的 commit 點右鍵 -
選擇 Reset ‘目前分支名’ to Here
-
選擇
reset
模式 -
完成
有沒有發現觀念看完,GUI 的操作根本就不會有疑問 XD
# 常見問題
# 1. 已經被移除的 commit ,還回得來嗎?
神隱少女中的湯婆婆說過一句話:「曾經發生的事不可能忘記,只是想不起來而已」
Git 的世界也是一樣 (哪裡一樣!),只要 commit 過的內容不會任意消失,除非你想不起來原本的 commitID…
如果真的不小心「後悔」 reset
的行為,有兩個復原方式:
第一種:用原本的 commitID 執行 hard reset。
- 尋找原本的 commitID:
應該沒人會在reset
之前先去記 ID,用reflog
找回來吧。
git reflog -5 |
reflog
是一個可以查看 HEAD 更變過程的特殊 log,執行跟 HEAD 有關的指令時,它就會記錄一筆,而 reset
就是這類的指令。
如果 reset
後忘記原本的 commitID ,用這個指令就可以查到。
指令後面多帶一個 -5
參數,是為了讓 reflog
列出最近五筆即可。
- 用這個 ID 執行 reset hard 模式
git reset --hard commitID |
第二種:用 ORIG_HEAD 執行 hard reset。
ORIG_HEAD 是 Git 的一個特殊指標,當我們執行一些 可能造成資料遺失 的指令時,ORIG_HEAD 就會自動幫我們記錄原先的 commitID,執行 reset
就是一個例子 (註 1)。
如果不慎執行 reset
希望還原回去時,可以直接用 ORIG_HEAD 來執行 reset
的 hard 模式:
git reset --hard ORIG_HEAD |
註 1. 除了 reset 指令之外,其他還有 merge 、 rebase 等操作,ORIG_HEAD 都會幫我們記錄原本的 CommitID
# 2. reset 指令有什麼需要注意的地方?
有,reset 之後又重新提交的 commit,ID 不會相同。
git reset
跟 git commit --amend
一樣都是會更改歷史的指令之一。
在講 --amend
時就有提到一個觀念,但必須再重申一次:
如果 commit 從沒推到遠端,想怎麼操作其實都不會影響到別人。
不過如果是跟他人共用的分支,隨意更改歷史之後,force push 回遠端,極有可能造成團隊成員的不便,不可不慎!!!
# 後記
reset
是個讓我又愛又恨的指令,當初為了搞懂他不知道燒爛了多少腦細胞…
什麼索引,什麼工作目錄,又捨棄又保留的,到底什麼意思…
在實際搞懂之後,發現 reset
除了上面說的那些基礎用途之外,還可以完很多花俏的操作,非常有趣 XD
文章刻意用好幾種角度來解釋 reset
的觀念,很希望能把自己的學習經驗,讓想搞懂 reset
的人能「感受」到 reset
能做到的事情。