簡述
版本控制系統可以幫助我們回到某個歷史版本,那么在 Git 里,如何回滾操作,撤銷某次提交或恢復到某個提交呢?以下我們根據需要回滾的常見場景來介紹這些操作的最佳辦法:
本地修改但未 push 到遠程
撤銷本地的修改
場景: 你修改了文件,但還沒有 commit 。你想要恢復到修改前 —— 就像上次 commit 的時候一模一樣。
方法: git checkout -- <bad filename>
原理: git checkout 會把工作目錄里的文件修改到 Git 之前記錄的某個狀態。你可以提供一個你想返回的分支名或特定 SHA ,或者在缺省情況下,Git 會認為你希望 checkout 的是 HEAD,當前 checkout 分支的最后一次 commit。
提醒:你用這種方法“撤銷”的任何修改真的會完全消失。因為它們從來沒有被提交過,所以之后 Git 也無法幫助我們恢復它們。你要確保自己了解你在這個操作里扔掉的東西是什么!(也許可以先利用 git diff 確認一下)
重置本地的修改
景: 你在本地 commit 了一些東西(還沒有 push),但是你發現這些提交是錯誤的不需要的,你希望撤銷前面的幾次提交 —— 就像它們從來沒有發生過一樣。
方法: git reset <last good SHA>
原理: git reset 會把你的代碼庫歷史返回到指定的 SHA 狀態。 這樣就像是這些提交從來沒有發生過。缺省情況下, git reset 會保留工作目錄。這樣,提交是沒有了,但是修改內容還在磁盤上。這是一種安全的選擇。
示例說明:以下我們模擬了 4 次提交,最近的 3 次提交是錯誤的,我們希望回到如下紅框處所示的“正確的提交”

第一步,我們找到要回到的這次提交的 commit,如上圖所示是 f5142d4e5a69a9250daa7612de399d78d0db1fd8
第二步,執行 git reset f5142d4e5a69a9250daa7612de399d78d0db1fd8

執行后,我們可以看到通過 3 次 commit 提交的內容取消暫存但仍然被保留到了工作目錄。你可以繼續編輯修正錯誤后,然后繼續通過 git add 、git commit 操作進行提交。
但通常我們會希望一步就“撤銷”提交以及修改內容 —— 這就是 --hard 選項的功能。但這個是一個危險操作,這是 reset 命令唯一的危險用法,--hard 選項會真正地銷毀數據,它強制覆蓋了工作目錄中的文件。只有當你確定廢棄修改時使用 --hard 選項。雖然我們可以通過下一節介紹的 reflog 來找回曾經 commit 過的,但對於未 commit 過的內容將無法恢復了。

在撤銷本地修改之后再恢復
場景: 你提交了幾個 commit,然后用 git reset --hard 撤銷了這些修改(見上一節使用了 --hard 選項),然后你意識到 reset 錯了,想要恢復。
方法: git reflog 和 git reset 或 git checkout
原理: git reflog 對於恢復項目歷史是一個超棒的資源。你可以恢復幾乎 任何東西 —— 任何你 commit 過的東西 —— 只要通過 reflog。
你可能已經熟悉了 git log 命令,它會顯示 commit 的列表。 git reflog 也是類似的,不過它顯示的是一個 HEAD 發生改變的時間列表.
注意事項:
- 它涉及的只是
HEAD的改變。在你切換分支、用git commit進行提交、以及用git reset撤銷 commit 時,HEAD會改變,但當你用git checkout -- <bad filename>撤銷時(正如我們在前面講到的情況),HEAD並不會改變 —— 如前所述,這些修改從來沒有被提交過,因此 reflog 也無法幫助我們恢復它們。
git reflog不會永遠保持。Git 會定期清理那些 “用不到的” 對象。不要指望幾個月前的提交還一直躺在那里。
reflog只存在於本地 —— 這是一個記錄你在你自己的倉庫里做過什么的日志。其他人拷貝的倉庫里的引用日志不會和你的相同;而你新克隆一個倉庫的時候,reflog 是空的,因為你在倉庫里還沒有操作。你的reflog就是你的,只是你的。你不能用git reflog來恢復另一個開發者沒有 push 過的 commit。
示例說明:通過 git reflog 命令可以查看引用日志

那么,怎么利用 reflog 來“恢復”之前“撤銷”的 commit 呢?它取決於你想做到的到底是什么:
- 如果你希望准確地恢復項目的歷史到某個時間點,用
git reset --hard <SHA>
- 如果你希望重建工作目錄里的一個或多個文件,讓它們恢復到某個時間點的狀態,用
git checkout <SHA> -- <filename>
- 如果你希望把這些 commit 里的某一個重新提交到你的代碼庫里,用
git cherry-pick <SHA>
修正最后一個 commit 消息
場景: 你在最后一條 commit 消息里有個筆誤,已經執行了 git commit -m "Fxies bug #42",但在 git push 之前你意識到消息應該是 “Fixes bug #42″。
方法: git commit --amend 或 git commit --amend -m "Fixes bug #42"
原理: git commit --amend 會用一個新的 commit 更新並替換最近的 commit ,這個新的 commit 會把任何修改內容和上一個 commit 的內容結合起來。如果當前沒有其他新的修改,這個操作就只會把上次的 commit 消息重寫一遍。如果你想通過添加或修改文件來更改提交的快照,也可以通過類似的操作來完成。 通過修改文件然后運行 git add或 git rm一個已追蹤的文件,隨后運行git commit --amend拿走當前的暫存區域並使其做為新提交的快照。
已經 push 到遠程
回滾其中的某個 commit
場景: 你已經執行了 git push, 把你的修改發送到了遠程,但是這些 commit 中其中一個是有問題的,你需要回滾那一個 commit.
方法: git revert <SHA>
原理: git revert 會產生一個新的 commit,它和指定 SHA 對應的 commit 是相反的(或者說是反轉的)。如果原先的 commit 是“物質”,新的 commit 就是“反物質” —— 任何從原先的 commit 里刪除的內容會在新的 commit 里被加回去,任何在原先的 commit 里加入的內容會在新的 commit 里被刪除。
這是 Git 最安全、最基本的撤銷場景,因為它並不會改變歷史 —— 所以你現在可以 git push 新的“反轉” commit 來抵消你錯誤提交的 commit。
示例說明:
第一步,確認要回滾的 commit。如下,需要回滾紅框的 commit 的內容,保留紅框上面的 commit 的內容,如要回滾 commit 3f4e41ee4c7ffe2a49ffab0343185c03e5c57f30

第二步,執行 git revert 3f4e41ee4c7ffe2a49ffab0343185c03e5c57f30

這時,可以看到產生了一個新的 commit
第三步,回滾完成后執行 git push 提交到遠程,遠程可以看到如下內容

回滾到某個 commit
場景: 你已經執行了 git push, 把你的修改發送到了遠程,但是這些 commit 都是有問題的,而且這些 commit 沒有合並線,你需要回滾到之前的 commit.(注:如果這些 commit 中有合並線,即有過其他合並,這個方法不適用)
方法: git revert HEAD..<SHA>
原理: git revert 會產生一個新的 commit,它和指定 SHA 對應的 commit 是相反的(或者說是反轉的)。如果原先的 commit 是“物質”,新的 commit 就是“反物質” —— 任何從原先的 commit 里刪除的內容會在新的 commit 里被加回去,任何在原先的 commit 里加入的內容會在新的 commit 里被刪除。
這是 Git 最安全、最基本的撤銷場景,因為它並不會改變歷史 —— 所以你現在可以 git push 新的“反轉” commit 來抵消你錯誤提交的 commit。
示例說明:
第一步,確認要回滾到的 commit。如下,需要回滾到紅框的 commit 的內容,也就是保留紅框里的 commit 的內容,要回滾到 88c5918b02c48c7f306559592c40aa1ffc3139fe 這個 commit

第二步,執行 git revert 88c5918b02c48c7f306559592c40aa1ffc3139fe..HEAD,HEAD 是最新的版本,這個表示要從 HEAD 開始,回滾到 88c5918b02c48c7f306559592c40aa1ffc3139fe 版本,也就是要逐個回滾上圖的 HEAD(59f0201fbf7940204dcffa7fdb951539f4934c9f)、983c12915afade3c7dbab4eab8d3a0ef1b0f0eb7、(如果中間還有的話)直到保留 88c5918b02c48c7f306559592c40aa1ffc3139fe 這個 commit

這時,可以看到產生了一個新的 commit
第三步,回滾完成后執行 git push 提交到遠程,遠程可以看到如下內容

回滾某次合並
場景: 你完成了一次合並,並執行了 git push, 把你的修改發送到了遠程,但是這次合並的內容是有問題的,你需要回滾到合並之前.
方法: git revert -m [要被保留下來的那個父節點] [要回滾的 commit]
-m [要被保留下來的那個父節點]標記指出 “mainline” 需要被保留下來的父結點。 當你完成一個合並,,新提交有兩個父結點:第一個是目標分支,第二個是要合並入目標分支的源分支的最新 commit。對於上述場景,如從 branch 合並到 master,要回滾 branch 合並到 master 的內容,這里就是要保留第一個父節點,也就是 git revert -m 1
原理: git revert 會產生一個新的 commit,它和指定 SHA 對應的 commit 是相反的(或者說是反轉的)。如果原先的 commit 是“物質”,新的 commit 就是“反物質” —— 任何從原先的 commit 里刪除的內容會在新的 commit 里被加回去,任何在原先的 commit 里加入的內容會在新的 commit 里被刪除。
這是 Git 最安全、最基本的撤銷場景,因為它並不會改變歷史 —— 所以你現在可以 git push 新的“反轉” commit 來抵消你錯誤提交的 commit。
示例說明:如從 branch 合並到 master,要回滾 branch 合並到 master 的內容
第一步,確認要回滾的合並的 commit。如下,需要回滾紅框的合並產生的 commit 的內容,這時合並產生的 commit 是 5a4228c92c359a53bc99cf17e68eebc59763fbc2

第二步,執行 git revert -m 1 5a4228c92c359a53bc99cf17e68eebc59763fbc2,-m 1指保留 master 的 commit,5a4228c92c359a53bc99cf17e68eebc59763fbc2 是要回滾的 commit

這時,可以看到產生了一個新的 commit
第三步,回滾完成后執行 git push 提交到遠程,遠程可以看到如下內容。由於我們只回滾合並操作,因此如下“merge 后的提交”是在合並后的提交,這個 commit 的修改內容不會被回滾。

回滾后,被回滾的合並的提交依然在提交歷史中。 如果你嘗試再次合並被回滾的 branch 分支, Git 會感到困惑:

這時,可以通過撤消那次回滾的的合並,然后再創建一個新的合並提交完成再次的合並操作,如上就是對回滾產生的 commit 進行 revert:

