前言
接下來進入重頭戲的部分,那就是版控系統絕對必備的檔案還原功能,前面我們有提到如何切換數據庫版本以做檢查,但你以為只有檢查嗎?它還可以還原阿!假設你今天開發應用時玩壞了某個檔案,透過簡單指令即可將檔案回復成未更動狀態,你說神不神奇?又或者是你發現了一個 Bug,為了修復這個 Bug,想了想還是把剛才提交的 commit 給拆掉重做比較快,這也同樣沒問題,只要你的本地數據庫沒有被刪掉,你想怎麼切、怎麼換、怎麼還原,通通不是問題。
筆記重點
- 檔案狀態的生命週期
- 工作目錄檔案還原
- 索引區檔案還原
- 還原至指定版本
- 使用 Reflog 查看還原紀錄
- Git 指令回顧
檔案狀態的生命週期
在我們正式進入到檔案還原章節之前,需先了解 Git 在各個工作流程所顯示的檔案狀態,請先開一個資料夾並新增 index.html
檔案:
1 | mkdir project |
此時使用 git status
查看目前檔案狀態:
此時 index.html
檔案狀態為 Untracked files
,代表此檔案從未加入過資料庫,且流程還處於工作目錄下,請先記錄下這一個狀態,接著繼續將這個檔案提交至索引區:
1 | git add . |
查看狀態:
此時的檔案狀態更改為了 Changes to be committed
,代表此檔案以處於索引區,即將提交至本地數據庫,你可能會想,下方的 new file
字樣需要記嗎?此字樣是代表此檔案從未加入過數據庫,且以處於索引區內,在後面執行還原操作時,是以檔案狀態為依據並輸入指定命令進行還原,也就代表像是 new file
或 modified
字樣,可記可不記,它只是告訴你將要執行的操作而已,讓我們正式將這個檔案提交至本地數據庫:
1 | git commit -m '新增 index.html' |
此時 index.html
檔案已被加入至本地數據庫,這也是我們之前在講解 Git 基本工作流程時的操作,接下來請隨意更改已存在本地數據庫的 index.html
內容,並使用 git status
查看狀態:
當已存在本地數據庫的檔案經過修改,此檔案就會回到工作目錄,須從跑一次 Git 工作流程,與從未存在本地數據庫的檔案差別在於檔案狀態為 Changes not staged for commit
,即代表檔案經過更動但還未提交至索引區或本地數據庫,讓我們來看此檔案提交至索引區後狀態為何:
此時檔案狀態更改為了 Changes to be committed
,與未曾提交至本地數據庫的檔案提交至索引區結果相同,差別在於操作動作更改為了 modified
字樣,代表修改的意思,讓我們先將此檔案提交至本地數據庫再來做總結:
1 | git commit -m '修改 index.html' |
跑過了上面的流程,可以做出以下總結:
Git 將尚未被提交的檔案分成了三個區塊:
Untracked files
:代表新創建的檔案從未加入至數據庫,所在區域為工作目錄Changes not staged for commit
:已存在數據庫的檔案經過修改回到工作目錄Changes to be committed
:從工作目錄提交至索引區的檔案
這三大狀態是我們下面使用檔案還原的關鍵,你可能會問,那狀態下的將執行操作會影響下面的範例嗎?答案是不會的,這些都屬於其他操作的範圍,先記這三大檔案狀態就好,接下來讓我們正式進入到檔案還原章節吧!
工作目錄檔案還原
經過了以上討論,代表存在工作目錄下的檔案只會有以下兩種狀態:
Untracked files
:新建立的檔案,還未被 Git 追蹤Changes not staged for commit
:已被 Git 追蹤,但檔案經過修改回到了工作目錄
針對以上兩種狀態,Git 分別提供不同指定以進行還原動作,讓我們先來看目前的日誌:
這兩個 commit 紀錄就是我們上面範例所產生的,接下來讓我們隨意新增一個檔案並查看狀態:
從上面結果可以得知,我們新增了一個 all.css
檔案,此時的檔案狀態為 Untracked files
,代表此檔案從未加入過數據庫,自然無法紀錄版本內容,那你可能會想,都還沒加入過數據庫,要如何進行檔案還原呢?是要還原什麼?事實上,處於這一狀態的檔案只能進行刪除動作,在這邊的還原就是指檔案還沒有被建立出來時候,讓我們先執行以下命令:
1 | git clean -n |
此時會跳出以下訊息:
這道命令的用意主要是讓我們得知那些檔案將被刪除,接著使用以下命令刪除檔案:
1 | git clean -f |
此時會跳出已刪除檔案的訊息:
使用 git clean
主要可針對 Untracked files
的檔案進行還原 (刪除) 動作,你可以去檢查剛剛新增的檔案是否還存在,接下來介紹 Changes not staged for commit
狀態的檔案如何進行還原,請先修改已存在數據庫的 index.html
檔案並查看狀態:
前面有提到已存在數據庫但被修改過的檔案狀態為 Changes not staged for commit
,此時如果你使用 git clean
是沒有任何效果的,git clean
只能作用在 Untrakced files
狀態下的檔案,針對此狀態,我們必須使用以下指令:
1 | git checkout index.html |
或:
1 | git checkout . |
此時會跳出以下訊息:
你可以針對單一檔案或全部檔案進行還原,還原的結果就是檔案還未修改的狀態。
以上就是針對存在於工作目錄階段的檔案如何進行還原的操作,可能會有人問,那假設我已經提交至索引區了呢?此時你就不能使用以上命令進行還原,必須使用 reset
命令才能還原,讓我們繼續看下去。
索引區檔案還原
索引區的檔案還原就比較簡單了,因為它只有以下狀態:
Changes to be committed
:從工作目錄提交至索引區的檔案
讓我們先隨意新增一個檔案並提交至索引區:
1 | touch all.js |
查看狀態:
如同前面所說,無論檔案是否被追蹤,只要提交至索引區,狀態就是 Changes to be committed
,此時可透過以下指令將此檔案還原至工作目錄:
1 | git reset HEAD --mixed |
--mixed
為預設模式,在這邊可以省略,此模式會把暫存區的檔案丟掉,但不會動到工作目錄的檔案,也就是說還原的檔案會留在工作目錄,但不會留在暫存區;而 HEAD 代表我們所要還原到的 commit 紀錄上,你不用想的這麼複雜,讓我們以 Sourcetree 來說明:
你會發現多出了一個 Uncommitted
節點,這一個節點即代表所有未經提交的紀錄,只要經過 git commit
此節點就會消失,進而生成一個有紀錄的 commit 節點,而上面的 HEAD
在這邊就是指 master
指向的這一個節點,之前我們有提到 HEAD
所代表的就是我們當下的狀態,所以上面命令你也可以寫成 git reset master
,代表將檔案還原至這一個節點,讓我們來看此時的結果為何:
檔案被還原到工作目錄了,由於此檔案當初是以新增的方式進入到 Git 工作流程,所以還原後的狀態才為 Untracked
,此時一樣可透過前面介紹的 git clean -f
將檔案進行還原 (刪除),如果是 Changes not staged for commit
狀態,就必須使用 git checkout .
來進行還原,是不是很簡單?
事實上,如果你想要一氣呵成將存在索引區的檔案直接還原到最初狀態,即 git reset HEAD + git clean -f
,你可以使用 reset
的另外一個參數:
1 | git reset HEAD --hard |
在 --hard
模式下,不管是工作目錄還是索引區的檔案都會被丟掉,這個模式在某些情況下特別好用,下面會再做介紹,到這邊我們也完成索引區的檔案還原囉。
還原至指定版本
在前面我們都是將狀態還原至 HEAD,也就是所謂的最新版本,那如果我們要還原到指定版本呢?比如說最新版本的前三個版本通通都不要了,我要將版本還原至最新版本數起的第四個版本,這時候該怎麼做?此時一樣可透過 reset
指令來完成,讓我們來複習一下各模式的處理方式:
--mixed
:預設的模式,還原後的檔案將丟回工作目錄--hard
:還原後的檔案將直接丟掉--soft
:還原後的檔案將丟回索引區
--soft
模式在之前沒有講到,我們在後面會再進行補充,讓我們先來看目前的日誌:
因為我們之前都是在做新增後還原的動作,所以 commit 記錄才會完全沒有改變,為了方便等等做版本還原的操作,請先隨意新增幾個 commit 紀錄:
1 | touch all.js |
查看目前的日誌:
版本還原有分所謂的相對路徑與絕對路徑,讓我們先來看相對對路徑的部分,假設我們要還原 HEAD
的前一個版本,也就是 8d8ee63
這一個 commit,請執行以下命令:
1 | git reset HEAD^ |
有沒有發現 ^
這一個符號?此符號代表你要還原幾個版本,如果是還原兩個版本,可以寫成 ^^
,那如果是還原五個版本呢?可以寫成 ^^^^^
,但這樣的寫法太累了,可以改使用 ~
符號代替,~5
代表還原五個版本,所以 ^
與 ~1
效果是相同的,讓我們來檢查日誌看是否還原成功:
此時你會發現當初的 e1e9db8
紀錄不見了,也就代表版本還原成功,你可能會好奇,那與 e1e9db8
有關的檔案呢?同樣也不見了嗎?讓我們來看看目前檔案狀態:
還記得之前 --mixed
模式的處理方式嗎?沒有錯,因為我們在使用 reset
指令時,並沒有加入任何的參數,預設就是使用 --mixed
模式,他把還原後的相關檔案全部丟到了工作目錄下,這才導致 e1e9db8
紀錄有關的檔案都存在於工作目錄下,此時一樣可透過 git clean -f
或 git checkout .
將檔案給還原到初始狀態。
你可能會問,我都已經還原到指定版本了,還需要一個一個看是什麼狀態並使用相對應的指令進行還原,這樣會不會太麻煩?還記得之前介紹地的 --hard
模式嗎?此模式可以把有關的檔案全部進行還原,就是全部丟掉的意思啦,假設我們以剛才的情況來說,回復到 HEAD
的前一個版本並使用 --hard
模式將有關的檔案全部丟掉,此時的狀態就會變為:
與 e1e9db8
有關的檔案就都被我們丟掉了,講解完了 --mixed
與 --hard
模式,大家應該就都知道 --soft
的用法了,沒錯,就是把有關的檔案全部都丟到索引區內,在這邊就不進行示範了,各位可以自己試試看。
上面是以相對路徑方式還原版本,你也可以使用絕對路徑的方式進行還原,先讓我們使用 git reflog
查看並還原到最一開始的狀態:
關於 Reflog
的使用方式,下面會再做介紹,先讓我們回到主題,目前的 commit 紀錄就是我們最一開始的狀態,假設我們要還原到 3c85d93
這一個節點,相對路徑寫法是 git reset HEAD~2
,而絕對路徑是寫成:
1 | gti reset 3c85d93 --hard |
不要懷疑,就是這麼簡單,只需要撰寫節點的 SHA-1 編碼即可,此時的檔案狀態為:
因為我們使用了 --hard
模式,所以與之相關的檔案都被我們丟掉了,是不是很方便阿?
使用 Reflog 查看還原紀錄
在前面我們有偷偷透過 git reflog
查看我們還原紀錄並還原,你可能會好奇,難道 reset 後的檔案可以 reset 回來?沒有錯,確實是可以的,我們拿前面的範例來看:
這是我們剛剛還原的結果,那假設我後悔了,我想要還原到剛剛還原前的狀態呢?此時一定有人會去查看剛剛還原前的最新 commit 紀錄,並且使用以下指令:
1 | git reset e1e9db8 |
沒錯!這樣子就可以還原到尚未使用還原指令前的狀態,但假如說你沒有記下那一個狀態的 SHA-1 值呢?是不是就不能還原了?當然還是可以啊!但必須透過 git reflog
找回這一個 SHA-1 標號:
當 HEAD
有移動時 (例如切換分支或還原版本),Git 就會在 Reflog 裡記上一筆,代表如果你做了任何傻事,都可以到這邊查找並進行復原,是不是很棒?這也是為什麼只要你曾將檔案加入過數據庫,絕大部份資料都可以找得回來的原因。
這樣子看起來,就算我們把 Git 給玩壞了,一樣可以使用 Reflog 還原到最初的狀態,但這邊要注意,Reflog 也是有保存時間的,預設來說 Git 會幫你保存這些歷史紀錄 90 天,如果這些紀錄中已經有些 commit 物件不在分支線上,則預設保留 30 天。但這些時間都是可以更改的,假如說你的硬碟無限大,永遠不想刪除紀錄,可以考慮設定如下:
1 | git config --global gc.reflogExpire 'never' |
在這邊補充一點,如果你想要查看 Reflog
內紀錄的時間,可以使用以下指令:
1 | git reflog --date=iso |
Git 指令回顧
1 | # 查看將被 git clean -f 還原的對象 |