現代化 Web 開發技術學習分享

0%

Git 版本控制系統 - 檔案還原與重置提交

前言

接下來進入重頭戲的部分,那就是版控系統絕對必備的檔案還原功能,前面我們有提到如何切換數據庫版本以做檢查,但你以為只有檢查嗎?它還可以還原阿!假設你今天開發應用時玩壞了某個檔案,透過簡單指令即可將檔案回復成未更動狀態,你說神不神奇?又或者是你發現了一個 Bug,為了修復這個 Bug,想了想還是把剛才提交的 commit 給拆掉重做比較快,這也同樣沒問題,只要你的本地數據庫沒有被刪掉,你想怎麼切、怎麼換、怎麼還原,通通不是問題。

筆記重點

  • 檔案狀態的生命週期
  • 工作目錄檔案還原
  • 索引區檔案還原
  • 還原至指定版本
  • 使用 Reflog 查看還原紀錄
  • Git 指令回顧

檔案狀態的生命週期

檔案狀態的生命週期

在我們正式進入到檔案還原章節之前,需先了解 Git 在各個工作流程所顯示的檔案狀態,請先開一個資料夾並新增 index.html 檔案:

1
2
3
4
5
6
7
mkdir project

cd project

git init

touch index.html

此時使用 git status 查看目前檔案狀態:

查看檔案狀態-1

此時 index.html 檔案狀態為 Untracked files,代表此檔案從未加入過資料庫,且流程還處於工作目錄下,請先記錄下這一個狀態,接著繼續將這個檔案提交至索引區:

1
git add .

查看狀態:

查看檔案狀態-2

此時的檔案狀態更改為了 Changes to be committed,代表此檔案以處於索引區,即將提交至本地數據庫,你可能會想,下方的 new file 字樣需要記嗎?此字樣是代表此檔案從未加入過數據庫,且以處於索引區內,在後面執行還原操作時,是以檔案狀態為依據並輸入指定命令進行還原,也就代表像是 new filemodified 字樣,可記可不記,它只是告訴你將要執行的操作而已,讓我們正式將這個檔案提交至本地數據庫:

1
git commit -m '新增 index.html'

此時 index.html 檔案已被加入至本地數據庫,這也是我們之前在講解 Git 基本工作流程時的操作,接下來請隨意更改已存在本地數據庫的 index.html 內容,並使用 git status 查看狀態:

查看檔案狀態-3

當已存在本地數據庫的檔案經過修改,此檔案就會回到工作目錄,須從跑一次 Git 工作流程,與從未存在本地數據庫的檔案差別在於檔案狀態為 Changes not staged for commit,即代表檔案經過更動但還未提交至索引區或本地數據庫,讓我們來看此檔案提交至索引區後狀態為何:

查看檔案狀態-4

此時檔案狀態更改為了 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 紀錄-1

這兩個 commit 紀錄就是我們上面範例所產生的,接下來讓我們隨意新增一個檔案並查看狀態:

查看檔案狀態-5

從上面結果可以得知,我們新增了一個 all.css 檔案,此時的檔案狀態為 Untracked files,代表此檔案從未加入過數據庫,自然無法紀錄版本內容,那你可能會想,都還沒加入過數據庫,要如何進行檔案還原呢?是要還原什麼?事實上,處於這一狀態的檔案只能進行刪除動作,在這邊的還原就是指檔案還沒有被建立出來時候,讓我們先執行以下命令:

1
git clean -n

此時會跳出以下訊息:

git clean -n

這道命令的用意主要是讓我們得知那些檔案將被刪除,接著使用以下命令刪除檔案:

1
git clean -f

此時會跳出已刪除檔案的訊息:

git clean -f

使用 git clean 主要可針對 Untracked files 的檔案進行還原 (刪除) 動作,你可以去檢查剛剛新增的檔案是否還存在,接下來介紹 Changes not staged for commit 狀態的檔案如何進行還原,請先修改已存在數據庫的 index.html 檔案並查看狀態:

查看檔案狀態-6

前面有提到已存在數據庫但被修改過的檔案狀態為 Changes not staged for commit,此時如果你使用 git clean 是沒有任何效果的,git clean 只能作用在 Untrakced files 狀態下的檔案,針對此狀態,我們必須使用以下指令:

1
git checkout index.html

或:

1
git checkout .

此時會跳出以下訊息:

git checkout index.html

你可以針對單一檔案或全部檔案進行還原,還原的結果就是檔案還未修改的狀態。

以上就是針對存在於工作目錄階段的檔案如何進行還原的操作,可能會有人問,那假設我已經提交至索引區了呢?此時你就不能使用以上命令進行還原,必須使用 reset 命令才能還原,讓我們繼續看下去。

索引區檔案還原

索引區的檔案還原就比較簡單了,因為它只有以下狀態:

  • Changes to be committed:從工作目錄提交至索引區的檔案

讓我們先隨意新增一個檔案並提交至索引區:

1
2
3
touch all.js

git add .

查看狀態:

查看檔案狀態-7

如同前面所說,無論檔案是否被追蹤,只要提交至索引區,狀態就是 Changes to be committed,此時可透過以下指令將此檔案還原至工作目錄:

1
git reset HEAD --mixed

--mixed 為預設模式,在這邊可以省略,此模式會把暫存區的檔案丟掉,但不會動到工作目錄的檔案,也就是說還原的檔案會留在工作目錄,但不會留在暫存區;而 HEAD 代表我們所要還原到的 commit 紀錄上,你不用想的這麼複雜,讓我們以 Sourcetree 來說明:

sourcetree

你會發現多出了一個 Uncommitted 節點,這一個節點即代表所有未經提交的紀錄,只要經過 git commit 此節點就會消失,進而生成一個有紀錄的 commit 節點,而上面的 HEAD 在這邊就是指 master 指向的這一個節點,之前我們有提到 HEAD 所代表的就是我們當下的狀態,所以上面命令你也可以寫成 git reset master,代表將檔案還原至這一個節點,讓我們來看此時的結果為何:

查看檔案狀態-8

檔案被還原到工作目錄了,由於此檔案當初是以新增的方式進入到 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 紀錄-2

因為我們之前都是在做新增後還原的動作,所以 commit 記錄才會完全沒有改變,為了方便等等做版本還原的操作,請先隨意新增幾個 commit 紀錄:

1
2
3
4
5
6
7
8
9
10
11
touch all.js

git add .

git commit -m '新增 all.js'

touch db.json

gti add .

git commit -m '新增 db.json'

查看目前的日誌:

查看 commit 紀錄-3

版本還原有分所謂的相對路徑與絕對路徑,讓我們先來看相對對路徑的部分,假設我們要還原 HEAD 的前一個版本,也就是 8d8ee63 這一個 commit,請執行以下命令:

1
git reset HEAD^

有沒有發現 ^ 這一個符號?此符號代表你要還原幾個版本,如果是還原兩個版本,可以寫成 ^^,那如果是還原五個版本呢?可以寫成 ^^^^^,但這樣的寫法太累了,可以改使用 ~ 符號代替,~5 代表還原五個版本,所以 ^~1 效果是相同的,讓我們來檢查日誌看是否還原成功:

查看 commit 紀錄-4

此時你會發現當初的 e1e9db8 紀錄不見了,也就代表版本還原成功,你可能會好奇,那與 e1e9db8 有關的檔案呢?同樣也不見了嗎?讓我們來看看目前檔案狀態:

查看檔案狀態-9

還記得之前 --mixed 模式的處理方式嗎?沒有錯,因為我們在使用 reset 指令時,並沒有加入任何的參數,預設就是使用 --mixed 模式,他把還原後的相關檔案全部丟到了工作目錄下,這才導致 e1e9db8 紀錄有關的檔案都存在於工作目錄下,此時一樣可透過 git clean -fgit checkout . 將檔案給還原到初始狀態。

你可能會問,我都已經還原到指定版本了,還需要一個一個看是什麼狀態並使用相對應的指令進行還原,這樣會不會太麻煩?還記得之前介紹地的 --hard 模式嗎?此模式可以把有關的檔案全部進行還原,就是全部丟掉的意思啦,假設我們以剛才的情況來說,回復到 HEAD 的前一個版本並使用 --hard 模式將有關的檔案全部丟掉,此時的狀態就會變為:

查看 commit 紀錄-4

e1e9db8 有關的檔案就都被我們丟掉了,講解完了 --mixed--hard 模式,大家應該就都知道 --soft 的用法了,沒錯,就是把有關的檔案全部都丟到索引區內,在這邊就不進行示範了,各位可以自己試試看。

上面是以相對路徑方式還原版本,你也可以使用絕對路徑的方式進行還原,先讓我們使用 git reflog 查看並還原到最一開始的狀態:

查看 commit 紀錄-5

關於 Reflog 的使用方式,下面會再做介紹,先讓我們回到主題,目前的 commit 紀錄就是我們最一開始的狀態,假設我們要還原到 3c85d93 這一個節點,相對路徑寫法是 git reset HEAD~2,而絕對路徑是寫成:

1
gti reset 3c85d93 --hard

不要懷疑,就是這麼簡單,只需要撰寫節點的 SHA-1 編碼即可,此時的檔案狀態為:

查看檔案狀態-10

因為我們使用了 --hard 模式,所以與之相關的檔案都被我們丟掉了,是不是很方便阿?

使用 Reflog 查看還原紀錄

在前面我們有偷偷透過 git reflog 查看我們還原紀錄並還原,你可能會好奇,難道 reset 後的檔案可以 reset 回來?沒有錯,確實是可以的,我們拿前面的範例來看:

查看 commit 紀錄-6

這是我們剛剛還原的結果,那假設我後悔了,我想要還原到剛剛還原前的狀態呢?此時一定有人會去查看剛剛還原前的最新 commit 紀錄,並且使用以下指令:

1
git reset e1e9db8

沒錯!這樣子就可以還原到尚未使用還原指令前的狀態,但假如說你沒有記下那一個狀態的 SHA-1 值呢?是不是就不能還原了?當然還是可以啊!但必須透過 git reflog 找回這一個 SHA-1 標號:

查看 commit 紀錄-7

HEAD 有移動時 (例如切換分支或還原版本),Git 就會在 Reflog 裡記上一筆,代表如果你做了任何傻事,都可以到這邊查找並進行復原,是不是很棒?這也是為什麼只要你曾將檔案加入過數據庫,絕大部份資料都可以找得回來的原因。

這樣子看起來,就算我們把 Git 給玩壞了,一樣可以使用 Reflog 還原到最初的狀態,但這邊要注意,Reflog 也是有保存時間的,預設來說 Git 會幫你保存這些歷史紀錄 90 天,如果這些紀錄中已經有些 commit 物件不在分支線上,則預設保留 30 天。但這些時間都是可以更改的,假如說你的硬碟無限大,永遠不想刪除紀錄,可以考慮設定如下:

1
2
git config --global gc.reflogExpire 'never'
git config --global gc.reflogExpireUnreachable 'never'

在這邊補充一點,如果你想要查看 Reflog 內紀錄的時間,可以使用以下指令:

1
git reflog --date=iso

Git 指令回顧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 查看將被 git clean -f 還原的對象
git clean -n

# 還原全部工作目錄檔案 (未追蹤)
git clean -f

# 還原指定工作目錄檔案 (以追蹤)
git checkout <file>

# 還原全部工作目錄檔案 (以追蹤)
git checkout .

# 還原索引區檔案 (丟回工作目錄)
git reset HEAD

# 還原索引區檔案 (同上,預設選項)
git reset HEAD --mixed

# 還原索引區檔案 (全部丟掉)
git reset HEAD --hard

# 還原至前兩個版本 (丟回工作目錄)
git reset HEAD^^ --mixed

# 還原至前三個版本 (丟回索引區)
git reset HEAD~3 --soft

# 還原至指定版本 (使用絕對路徑,全部丟掉)
git reset <SHA-1> --hard

# 查看還原紀錄
git reflog

# 查看還原紀錄 (顯示時間)
git reflog --date=iso

# 修改 reflog 保存時間 (存在分支線上,預設 90 天改為無限)
git config --global gc.reflogExpire 'never'

# 修改 reflog 保存時間 (不存在分支線上,預設 30 天改為無限)
git config --global gc.reflogExpireUnreachable 'never'