任何版本控制系統的一個最有的用特性就是「撤銷 (undo)」你的錯誤操做的能力。在 Git 裏,「撤銷」 蘊含了很多略有差異的功能。linux
當你進行一次新的提交的時候,Git 會保存你代碼庫在那個特定時間點的快照;以後,你能夠利用 Git 返回到你的項目的一個早期版本。git
在本篇博文裏,我會講解某些你須要「撤銷」已作出的修改的常見場景,以及利用 Git 進行這些操做的最佳方法。安全
場景: 你已經執行了 git push
, 把你的修改發送到了 GitHub,如今你意識到這些 commit 的其中一個是有問題的,你須要撤銷那一個 commit.app
方法: git revert <SHA>
編輯器
原理: git revert
會產生一個新的 commit,它和指定 SHA 對應的 commit 是相反的(或者說是反轉的)。若是原先的 commit 是「物質」,新的 commit 就是「反物質」 — 任何從原先的 commit 裏刪除的內容會在新的 commit 裏被加回去,任何在原先的 commit 里加入的內容會在新的 commit 裏被刪除。spa
這是 Git 最安全、最基本的撤銷場景,由於它並不會改變歷史 — 因此你如今能夠 git push
新的「反轉」 commit 來抵消你錯誤提交的 commit。版本控制
場景: 你在最後一條 commit 消息裏有個筆誤,已經執行了 git commit -m "Fxies bug #42"
,但在 git push
以前你意識到消息應該是 「Fixes bug #42″。code
方法: git commit --amend
或 git commit --amend -m "Fixes bug #42"
orm
原理: git commit --amend
會用一個新的 commit 更新並替換最近的 commit ,這個新的 commit 會把任何修改內容和上一個 commit 的內容結合起來。若是當前沒有提出任何修改,這個操做就只會把上次的 commit 消息重寫一遍。 對象
場景: 一隻貓從鍵盤上走過,無心中保存了修改,而後破壞了編輯器。不過,你尚未 commit 這些修改。你想要恢復被修改文件裏的全部內容 — 就像上次 commit 的時候如出一轍。
方法: git checkout -- <bad filename>
原理: git checkout
會把工做目錄裏的文件修改到 Git 以前記錄的某個狀態。你能夠提供一個你想返回的分支名或特定 SHA ,或者在缺省狀況下,Git 會認爲你但願 checkout 的是 HEAD
,當前 checkout 分支的最後一次 commit。
記住:你用這種方法「撤銷」的任何修改真的會徹底消失。由於它們歷來沒有被提交過,因此以後 Git 也沒法幫助咱們恢復它們。你要確保本身瞭解你在這個操做裏扔掉的東西是什麼!(也許能夠先利用 git diff
確認一下)
場景: 你在本地提交了一些東西(尚未 push),可是全部這些東西都很糟糕,你但願撤銷前面的三次提交 — 就像它們歷來沒有發生過同樣。
方法: git reset <last good SHA>
或 git reset --hard <last good SHA>
原理: git reset
會把你的代碼庫歷史返回到指定的 SHA 狀態。 這樣就像是這些提交歷來沒有發生過。缺省狀況下, git reset
會保留工做目錄。這樣,提交是沒有了,可是修改內容還在磁盤上。這是一種安全的選擇,但一般咱們會但願一步就「撤銷」提交以及修改內容 — 這就是 --hard
選項的功能。
場景: 你提交了幾個 commit,而後用 git reset --hard
撤銷了這些修改(見上一段),接着你又意識到:你但願還原這些修改!
方法: 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
就是你的,只是你的。你不能用 git reflog
來恢復另外一個開發者沒有 push 過的 commit。
那麼…你怎麼利用 reflog 來「恢復」以前「撤銷」的 commit 呢?它取決於你想作到的究竟是什麼:
若是你但願準確地恢復項目的歷史到某個時間點,用 git reset --hard <SHA>
若是你但願重建工做目錄裏的一個或多個文件,讓它們恢復到某個時間點的狀態,用 git checkout <SHA> -- <filename>
若是你但願把這些 commit 裏的某一個從新提交到你的代碼庫裏,用 git cherry-pick <SHA>
場景: 你進行了一些提交,而後意識到你開始 check out 的是 master
分支。你但願這些提交進到另外一個特性(feature)分支裏。
方法: git branch feature
, git reset --hard origin/master
, and git checkout feature
原理: 你可能習慣了用 git checkout -b <name>
建立新的分支 — 這是建立新分支並立刻 check out 的流行捷徑 — 可是你不但願立刻切換分支。這裏, git branch feature
建立一個叫作 feature
的新分支並指向你最近的 commit,但仍是讓你 check out 在 master 分支上。
下一步,在提交任何新的 commit 以前,用 git reset --hard
把 master
分支倒回 origin/master
。不過別擔憂,那些 commit 還在 feature
分支裏。
最後,用 git checkout
切換到新的 feature
分支,而且讓你最近全部的工做成果都無缺無損。
場景: 你在 master 分支的基礎上建立了 feature
分支,但 master
分支已經滯後於 origin/master
不少。如今 master
分支已經和 origin/master
同步,你但願在 feature
上的提交是從如今開始,而不是也從滯後不少的地方開始。
方法: git checkout feature
和 git rebase master
原理: 要達到這個效果,你原本能夠經過 git reset
(不加 --hard
, 這樣能夠在磁盤上保留修改) 和 git checkout -b <new branch name>
而後再從新提交修改,不過這樣作的話,你就會失去提交歷史。咱們有更好的辦法。
git rebase master
會作以下的事情:
首先它會找到你當前 check out 的分支和 master
分支的共同祖先。
而後它 reset 當前 check out 的分支到那個共同祖先,在一個臨時保存區存放全部以前的提交。
而後它把當前 check out 的分支提到 master
的末尾部分,並從臨時保存區從新把存放的 commit 提交到 master
分支的最後一個 commit 以後。
場景: 你向某個方向開始實現一個特性,可是半路你意識到另外一個方案更好。你已經進行了十幾回提交,但你如今只須要其中的一部分。你但願其餘不須要的提交通通消失。
方法: git rebase -i <earlier SHA>
原理: -i
參數讓 rebase
進入「交互模式」。它開始相似於前面討論的 rebase,但在從新進行任何提交以前,它會暫停下來並容許你詳細地修改每一個提交。
rebase -i
會打開你的缺省文本編輯器,裏面列出候選的提交。以下所示:
前面兩列是鍵:第一個是選定的命令,對應第二列裏的 SHA 肯定的 commit。缺省狀況下, rebase -i
假定每一個 commit 都要經過 pick
命令被運用。
要丟棄一個 commit,只要在編輯器裏刪除那一行就好了。若是你再也不須要項目裏的那幾個錯誤的提交,你能夠刪除上例中的一、三、4行。
若是你須要保留 commit 的內容,而是對 commit 消息進行編輯,你可使用 reword
命令。 把第一列裏的 pick
替換爲 reword
(或者直接用 r
)。有人會以爲在這裏直接重寫 commit 消息就好了,可是這樣無論用 —rebase -i
會忽略 SHA 列前面的任何東西。它後面的文本只是用來幫助咱們記住 0835fe2
是幹啥的。當你完成 rebase -i
的操做以後,你會被提示輸入須要編寫的任何 commit 消息。
若是你須要把兩個 commit 合併到一塊兒,你可使用 squash
或 fixup
命令,以下所示:
squash
和 fixup
會「向上」合併 — 帶有這兩個命令的 commit 會被合併到它的前一個 commit 裏。在這個例子裏, 0835fe2
和 6943e85
會被合併成一個 commit, 38f5e4e
和 af67f82
會被合併成另外一個。
若是你選擇了 squash,
Git 會提示咱們給新合併的 commit 一個新的 commit 消息; fixup
則會把合併清單裏第一個 commit 的消息直接給新合併的 commit 。 這裏,你知道 af67f82
是一個「完了完了….」 的 commit,因此你會留着 38f5e4e
的 commit 消息,但你會給合併了 0835fe2
和 6943e85
的新 commit 編寫一個新的消息。
在你保存並退出編輯器的時候,Git 會按從頂部到底部的順序運用你的 commit。你能夠經過在保存前修改 commit 順序來改變運用的順序。若是你願意,你也能夠經過以下安排把 af67f82
和 0835fe2
合併到一塊兒:
場景: 你在一個更早期的 commit 裏忘記了加入一個文件,若是更早的 commit 能包含這個忘記的文件就太棒了。你尚未 push,但這個 commit 不是最近的,因此你無法用 commit --amend
.
方法: git commit --squash <SHA of the earlier commit>
和 git rebase --autosquash -i <even earlier SHA>
原理: git commit --squash
會建立一個新的 commit ,它帶有一個 commit 消息,相似於 squash! Earlier commit
。 (你也能夠手工建立一個帶有相似 commit 消息的 commit,可是 commit --squash
能夠幫你省下輸入的工做。)
若是你不想被提示爲新合併的 commit 輸入一條新的 commit 消息,你也能夠利用 git commit --fixup
。在這個狀況下,你極可能會用commit --fixup
,由於你只是但願在 rebase
的時候使用早期 commit 的 commit 消息。
rebase --autosquash -i
會激活一個交互式的 rebase
編輯器,可是編輯器打開的時候,在 commit 清單裏任何 squash!
和 fixup!
的 commit 都已經配對到目標 commit 上了,以下所示:
在使用 --squash
和 --fixup
的時候,你可能不記得想要修正的 commit 的 SHA 了— 只記得它是前面第 1 個或第 5 個 commit。你會發現 Git 的 ^
和 ~
操做符特別好用。HEAD^
是 HEAD
的前一個 commit。 HEAD~4
是 HEAD
往前第 4 個 – 或者一塊兒算,倒數第 5 個 commit。
場景: 你偶然把 application.log
加到代碼庫裏了,如今每次你運行應用,Git 都會報告在 application.log
裏有未提交的修改。你把 *.log
in 放到了 .gitignore
文件裏,可文件仍是在代碼庫裏 — 你怎麼才能告訴 Git 「撤銷」 對這個文件的追蹤呢?
方法: git rm --cached application.log
原理: 雖然 .gitignore
會阻止 Git 追蹤文件的修改,甚至不關注文件是否存在,但這只是針對那些之前歷來沒有追蹤過的文件。一旦有個文件被加入並提交了,Git 就會持續關注該文件的改變。相似地,若是你利用 git add -f
來強制或覆蓋了 .gitignore
, Git 還會持續追蹤改變的狀況。以後你就沒必要用-f
來添加這個文件了。
若是你但願從 Git 的追蹤對象中刪除那個本應忽略的文件, git rm --cached
會從追蹤對象中刪除它,但讓文件在磁盤上保持原封不動。由於如今它已經被忽略了,你在 git status
裏就不會再看見這個文件,也不會再偶然提交該文件的修改了。
這就是如何在 Git 裏撤銷任何操做的方法。要了解更多關於本文中用到的 Git 命令,請查看下面的有關文檔: