在使用Git進行程式專案或是其它任何專案的版本控制時,有時候可能會因為一些意外而導致專案發生了不應該出現的變動,這時候要如何利用Git來還原呢?在這篇文章中,將會介紹如何用git checkoutgit resetgit revert這三種指令來分別處理不同的狀況。



示範用的專案

之後的內容將會一直使用以下GitHub連結的專案來做示範:

https://github.com/magiclen/git-recovery-practice

各位看官可以用以下指令將它clone下來:

git clone https://github.com/magiclen/git-recovery-practice.git

復原還沒add的變動

plain.txt的第9行改成90,不過90放在這行顯然不怎麼合理,所以我們想復原這個變動,該怎麼做呢?

git-recovery

所幸我們還沒有執行git add指令去「stage」這個變動,只要執行以下指令就可以把目前的專案還原成當前的分支(branch)最新的commit。

git checkout -f

git-recovery

git checkout指令能夠只還原一個或多個「unstaged」(add之前的狀態)的檔案,不過不是很常用,用法如下:

git checkout [<commit>] -- <檔案在Git倉庫中的路徑> <檔案2在Git倉庫中的路徑>

以上指令中,commit的部份是可選的,如果不填的話就會使用當前的分支最新的commit。如果填別的commit的話,還原回來的檔案可能會與當前的分支最新的commit不合,此時的檔案會是「staged」狀態。

例如:

git checkout master -- plain.txt

如果要還原該層工作目錄下的所有「unstaged」的變動,用法如下:

git checkout [<commit>] -- .

復原已經add但還沒commit的變動

這個狀況同樣可以用git checkout -f指令來解決。

git-recovery

但如果我們想要保留這個變動,只是要讓它從「staged」狀態變回「unstaged」狀態。可以使用以下指令:

git reset

git-recovery

git reset指令能夠只將一個或多個檔案的狀態改從「staged」變回「unstaged」。用法如下:

git reset -- <檔案1在Git倉庫中的路徑> <檔案2在Git倉庫中的路徑>

復原已經commit但還沒push的變動

git reset指令可以使當前的分支移動到某個commit,並保留當前的檔案為「unstaged」狀態。用法如下:

git reset <commit>

git-recovery

下圖是在演示git reset指令在預設模式(mixed)下,會保留當前的檔案變動為「unstaged」狀態。

git-recovery

如果不小心把分支移動錯了,可以使用git reflog指令來查看commit,然後再使用一次git reset指令把分支移動到正確的commit上。

git-recovery

git reset指令加上--hard參數,就不會保留當前的檔案變動。

git-recovery

下圖是在演示git reset指令在hard模式下,不會保留當前的檔案變動。

git-recovery

git reset指令加上--soft參數,會分別保留當前「unstaged」狀態和「staged」狀態的檔案變動。已經commit過的變動為「staged」狀態。

git-recovery

下圖是在演示git reset指令在soft模式下,分別保留當前不同狀態的檔案變動。

git-recovery

除了用git reset指令來復原分支外,也還可以用git checkout指令跳到某個指定的commit上,再用git branch -f指令將分支強制移動到這個commit的方式來完成復原。

例如要從commit add 8復原到add 5,我們可以先利用tig指令工具或是git log --format='%h %s'來找到add 5的雜湊值(長的短的都行)。

git-recovery

如下圖,首先用git branch指令確認目前的分支是master,然後用git checkout <commit>指令跳到commit add 5上,再用git branch -f master指令將master分支強制移動到這個commit,最後用git checkout master指令回到master分支。

git-recovery

復原已經commitpush的變動

雖然我們可以直接用上一個小節的方式來復原分支,並使用git push -f指令來強制覆蓋遠端Git倉庫上的檔案,如果這個專案只有我們自己一個人開發倒還行,但如果是多人開發的話,這樣的作法不是很好,容易搞爛其它共同開發者的Git。故比較好的處理方式是:在已經變動的基礎下,再去產生新的commit來復原變動。(復原變動所產生的變動也是變動。)

git revert指令可以反轉指定commit的變動,並且產生新的commit。用法如下:

git revert <commit>

假設我們要從add 8這個commit反轉3個commit(add 8add 7add 6),指令如下:

git revert HEAD
git revert HEAD~2
git revert HEAD~4

由於每次git revert指令都會產生新的commit,所以commit次序會是HEADHEAD~2HEAD~4(數字加2)。如果要再多反轉1個commit,那就要反轉HEAD~6

git-recovery

如果不想要每次反轉就產生一個commit出來,可以在使用git revert指令時加上--no-commit參數,最後再一起commit就好(不需要使用add)。

假設我們要從add 8這個commit反轉3個commit(add 8add 7add 6),指令如下:

git revert --no-commit HEAD
git revert --no-commit HEAD~1
git revert --no-commit HEAD~2
git commit

git-recovery

git revert指令其實也有提供一個方便的語法,來一次反轉指定範圍內所有commit。用法如下:

git revert <commit_start>..[<commit_end>]

以上的<commit_start>本身所指的commit不會被反轉,實際上會從該commit的下一個commit開始進行反轉。若不設定<commit_end>,預設是HEAD

假設我們要從add 8這個commit反轉3個commit(add 8add 7add 6),指令如下:

git revert HEAD~3..

git-recovery

同樣地,如果不想要每次反轉就產生一個commit出來,可以在使用git revert指令時加上--no-commit參數,最後再一起commit就好。

git revert --no-commit HEAD~3..
git commit

git-recovery

如果要反轉的<commit_end><commit_start>(不包含)之間有合併分支的commit存在,就需要分段反轉。因為我們必須讓Git知道在反轉合併分支的commit時,要往哪個分支走。

假設我們要從add 8這個commit反轉回add 4的狀態,首先要用tig指令工具查看在這段反轉路線中,有沒有叉路出現。找出合併分支的commit的雜湊值,以及反轉後要走的方向。

git-recovery

然後輸入以下指令:

git revert --no-commit <merge commit>..
git revert --no-commit -m 1 <merge commit>

以上指令的-m 1,是要讓git revert指令在反轉合併分支的commit之後往左邊走。如果要往右邊,就要改成-m 2

git-recovery

如上圖,此時的專案已經反轉回add 5的狀態。

然後找出合併分支的commit往前至左邊路線的第一個commit的雜湊值,以及add 4的commit雜湊值。

git-recovery

git-recovery

繼續用git revert指令做反轉,就可以成功回到add 4的狀態啦!

git-recovery