在使用Git進行程式專案或是其它任何專案的版本控制時,有時候可能會因為一些意外而導致專案發生了不應該出現的變動,這時候要如何利用Git來還原呢?在這篇文章中,將會介紹如何用git checkout
、git reset
、git revert
這三種指令來分別處理不同的狀況。
示範用的專案
之後的內容將會一直使用以下GitHub連結的專案來做示範:
各位看官可以用以下指令將它clone
下來:
復原還沒add
的變動
將plain.txt
的第9行改成90
,不過90
放在這行顯然不怎麼合理,所以我們想復原這個變動,該怎麼做呢?
所幸我們還沒有執行git add
指令去「stage」這個變動,只要執行以下指令就可以把目前的專案還原成當前的分支(branch)最新的commit。
git checkout
指令能夠只還原一個或多個「unstaged」(add
之前的狀態)的檔案,不過不是很常用,用法如下:
以上指令中,commit的部份是可選的,如果不填的話就會使用當前的分支最新的commit。如果填別的commit的話,還原回來的檔案可能會與當前的分支最新的commit不合,此時的檔案會是「staged」狀態。
例如:
如果要還原該層工作目錄下的所有「unstaged」的變動,用法如下:
復原已經add
但還沒commit
的變動
這個狀況同樣可以用git checkout -f
指令來解決。
但如果我們想要保留這個變動,只是要讓它從「staged」狀態變回「unstaged」狀態。可以使用以下指令:
git reset
指令能夠只將一個或多個檔案的狀態改從「staged」變回「unstaged」。用法如下:
復原已經commit
但還沒push
的變動
git reset
指令可以使當前的分支移動到某個commit,並保留當前的檔案為「unstaged」狀態。用法如下:
下圖是在演示git reset
指令在預設模式(mixed)下,會保留當前的檔案變動為「unstaged」狀態。
如果不小心把分支移動錯了,可以使用git reflog
指令來查看commit,然後再使用一次git reset
指令把分支移動到正確的commit上。
git reset
指令加上--hard
參數,就不會保留當前的檔案變動。
下圖是在演示git reset
指令在hard模式下,不會保留當前的檔案變動。
git reset
指令加上--soft
參數,會分別保留當前「unstaged」狀態和「staged」狀態的檔案變動。已經commit過的變動為「staged」狀態。
下圖是在演示git reset
指令在soft模式下,分別保留當前不同狀態的檔案變動。
除了用git reset
指令來復原分支外,也還可以用git checkout
指令跳到某個指定的commit上,再用git branch -f
指令將分支強制移動到這個commit的方式來完成復原。
例如要從commit add 8
復原到add 5
,我們可以先利用tig
指令工具或是git log --format='%h %s'
來找到add 5
的雜湊值(長的短的都行)。
如下圖,首先用git branch
指令確認目前的分支是master
,然後用git checkout <commit>
指令跳到commit add 5
上,再用git branch -f master
指令將master
分支強制移動到這個commit,最後用git checkout master
指令回到master
分支。
復原已經commit
並push
的變動
雖然我們可以直接用上一個小節的方式來復原分支,並使用git push -f
指令來強制覆蓋遠端Git倉庫上的檔案,如果這個專案只有我們自己一個人開發倒還行,但如果是多人開發的話,這樣的作法不是很好,容易搞爛其它共同開發者的Git。故比較好的處理方式是:在已經變動的基礎下,再去產生新的commit來復原變動。(復原變動所產生的變動也是變動。)
git revert
指令可以反轉指定commit的變動,並且產生新的commit。用法如下:
假設我們要從add 8
這個commit反轉3個commit(add 8
、add 7
、add 6
),指令如下:
git revert HEAD
git revert HEAD~2
git revert HEAD~4
由於每次git revert
指令都會產生新的commit,所以commit次序會是HEAD
、HEAD~2
、HEAD~4
(數字加2
)。如果要再多反轉1個commit,那就要反轉HEAD~6
。
如果不想要每次反轉就產生一個commit出來,可以在使用git revert
指令時加上--no-commit
參數,最後再一起commit
就好(不需要使用add
)。
假設我們要從add 8
這個commit反轉3個commit(add 8
、add 7
、add 6
),指令如下:
git revert --no-commit HEAD
git revert --no-commit HEAD~1
git revert --no-commit HEAD~2
git commit
git revert
指令其實也有提供一個方便的語法,來一次反轉指定範圍內所有commit。用法如下:
以上的<commit_start>
本身所指的commit不會被反轉,實際上會從該commit的下一個commit開始進行反轉。若不設定<commit_end>
,預設是HEAD
。
假設我們要從add 8
這個commit反轉3個commit(add 8
、add 7
、add 6
),指令如下:
git revert HEAD~3..
同樣地,如果不想要每次反轉就產生一個commit出來,可以在使用git revert
指令時加上--no-commit
參數,最後再一起commit
就好。
git revert --no-commit HEAD~3..
git commit
如果要反轉的<commit_end>
到<commit_start>
(不包含)之間有合併分支的commit存在,就需要分段反轉。因為我們必須讓Git知道在反轉合併分支的commit時,要往哪個分支走。
假設我們要從add 8
這個commit反轉回add 4
的狀態,首先要用tig
指令工具查看在這段反轉路線中,有沒有叉路出現。找出合併分支的commit的雜湊值,以及反轉後要走的方向。
然後輸入以下指令:
git revert --no-commit <merge commit>..
git revert --no-commit -m 1 <merge commit>
以上指令的-m 1
,是要讓git revert
指令在反轉合併分支的commit之後往左邊走。如果要往右邊,就要改成-m 2
。
如上圖,此時的專案已經反轉回add 5
的狀態。
然後找出合併分支的commit往前至左邊路線的第一個commit的雜湊值,以及add 4
的commit雜湊值。
繼續用git revert
指令做反轉,就可以成功回到add 4
的狀態啦!