Git + GitHub 版本控制教學 (4) - 回復到之前的版本:checkout、reset、revert

既然我們在 git 裡面獲得了存檔(commit)的功能,就表示 git 也提供了讓我們可以回到過去的能力。使用 reset、revert、checkout 可以讓我們回到過去的版本,本篇文章會介紹這些指令的差異以及使用的時機。

Git + GitHub 版本控制教學 (4) - 回復到之前的版本:checkout、reset、revert

既然我們在 git 裡面獲得了存檔(commit)的功能,就表示 git 也提供了讓我們可以回到過去的能力。使用 reset、revert、checkout 可以讓我們回到過去的版本,本篇文章會介紹這些指令的差異以及使用的時機。

HEAD 以及 commit id

在開發到一個段落以後,累積了一定的 commit 量。我們需要類似指標的東西讓我們得以分辨每一個版本。最直覺的就是版本編號: commit id,每個版本都會有它獨特的 commit id,長得會類似這樣:

6be9cb5e8d1ed532400ec6a2daa00e83357a96bf

幾乎在任何情況下我們都不會使用完整的 commit id,我們只需要使用最前面幾個字表示 git 就能自動判別。上面的例子來說:縮短版可以是 6be9cb56be9cb5e6be9cb5e8 等。

HEAD 是一個指標,告訴我們現在是在整個 git 歷史的哪個位置、哪個分支。 HEAD 會指到你當前分支最新的那個 commit。但是你也可以讓 HEAD 指到任何一個 commit,這種情況下我們稱它為 detached HEAD。

我們可以在 project 底下查看 .git/HEAD 來得知我們目前的 HEAD 在哪裏:

# 在cmd上下cat指令:
cat .git/HEAD

# 結果有可能如下:

# 顯示你正在 master 這個 branch
ref: refs/heads/master

# 顯示你正在 feature1 這個 branch
ref: refs/heads/feature1

# 顯示你正在 feature1 這個 branch
ref: refs/heads/feature1

# 顯示你正在以下這個 commit (detached HEAD)
6be9cb5e8d1ed532400ec6a2daa00e83357a96bf

git checkout

了解了 HEAD 以後, checkout 的概念就變得非常簡單。使用 checkout 其實就是移動 HEAD 的位置。

# 切換到 master 這個 branch 
# HEAD 指到 master branch 最新的 commit
git checkout master

# 切換到 feature 這個 branch 
# HEAD 指到 feature branch 最新的 commit
git checkout feature

# 切換到 commit 6be9cb5e8d1ed532400ec6a2daa00e83357a96bf 
# HEAD 只到這個 commit (detached HEAD)
git checkout 6be9cb5

因為 checkout 做的事情只是移動 HEAD,所以並不會對既有的 commit 作出任何修改,可以把它想像成“查看”的動作。如上圖指令所示,你可以用 checkout 切換你現在正在的 branch,也可以切換到任一個 commit。 detached HEAD 有點像是暫時的 branch,離開時會被刪除。

detached HEAD

git reset

reset 一樣可以回復到之前的版本,但它不像 checkout 會開一個暫時的 detached HEAD,它會實際把當下的 branch 版本退回到你要回到的地方(移動 HEAD)。通常使用 reset 的情境是:我最近的一些程式改動突然某些原因不想要了,但我已經提交了幾個 commit,這時候就可以使用 reset 回到這些 commit 前的版本。

# 可以用 ~n 表示回到HEAD的前n個版本
git reset HEAD~1
git reset HEAD~2
...

# HEAD^ 也意指HEAD的前一個版本
git reset HEAD^

# 可以直接使用 commit id
git reset 6be9cb5

用了 reset 以後會察覺 code 沒有並沒有變。這是因為 git 預設會做 git reset --mixed xxx 。雖然 commit 回到過去的版本,但版本之後的這些改動並不會消失,而是回到了 working directory (working directory 參考 https://june.monster/git-commit/)。我認為這是一種保護機制,可以先讓你看看兩個版本的差異。

git reset = git reset --mixed

如果要真正的回到過去的某個 commit ,可以使用  git reset --hard xxx--hard 不會把程式改動再放回 working directory。

git revert

revert 跟另外兩個指令又有不同,它回復版本的方式並不是去移動 HEAD。它會幫我們製造一個新的 commit,這個 commit 會把我們要 revert 的 commit 的內容給逆向操作一遍。

# 把目前所在的 branch 裡最新的 commit 給逆向操作一遍
# 做完後會發現多一個 commit,裡面內容是復原最新 commit 的改動
git revert HEAD

# 新增一個 commit,內容是反向去復原 6be9cb5 做的改動
git revert 6be9cb5
git revert

上面例子可以看到:原本最新的 commit 是 bb806ee (message: test commit), revert 後 git 自動幫我們加上 b0fa88c  (message: Revert "test commit") 這個新的 commit。

如果是個人 project 的話,用  revert 其實有些 over kill。我們可以用 reset 讓 commit history 看起來更乾淨。使用 revert 的時機是當我們跟他人合作時,我們會共同對遠端的一個 repo (可能是 Github、Gitlab...) 做貢獻。一但你的 commit 已經推到了遠端後再用  reset ,會改變原本 branch 的歷史。 這時候用 revert 就是很好的選擇。

我們最常用到的地方會是 Production 的 rollback。當團隊裡有人的程式改動造成嚴重 bug 而且沒法立刻找出原因的話,我們就會把這個有問題的 commit 給 revert 而達到 rollback 的效果。