如何在 Git 裏撤銷(幾乎)任何操做

任何版本控制系統的一個最有的用特性就是「撤銷 (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 消息

場景: 你在最後一條 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

那麼…你怎麼利用 reflog 來「恢復」以前「撤銷」的 commit 呢?它取決於你想作到的究竟是什麼:

  • 若是你但願準確地恢復項目的歷史到某個時間點,用 git reset --hard <SHA>

  • 若是你但願重建工做目錄裏的一個或多個文件,讓它們恢復到某個時間點的狀態,用 git checkout <SHA> -- <filename>

  • 若是你但願把這些 commit 裏的某一個從新提交到你的代碼庫裏,用 git cherry-pick <SHA>

利用分支的另外一種作法

場景: 你進行了一些提交,而後意識到你開始 check out 的是 master 分支。你但願這些提交進到另外一個特性(feature)分支裏。

方法: git branch featuregit 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 會打開你的缺省文本編輯器,裏面列出候選的提交。以下所示:

rebase-interactive1

前面兩列是鍵:第一個是選定的命令,對應第二列裏的 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 命令,以下所示:

rebase-interactive2

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 合併到一塊兒:

rebase-interactive3

修復更早期的 commit

場景: 你在一個更早期的 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 上了,以下所示:

rebase-autosquash

在使用 --squash 和 --fixup 的時候,你可能不記得想要修正的 commit 的 SHA 了— 只記得它是前面第 1 個或第 5 個 commit。你會發現 Git 的 ^ 和 操做符特別好用。HEAD^ 是 HEAD 的前一個 commit。 HEAD~4 是 HEAD 往前第 4 個 – 或者一塊兒算,倒數第 5 個 commit。

中止追蹤一個文件

場景: 你偶然把 application.log 加到代碼庫裏了,如今每次你運行應用,Git 都會報告在 application.log 裏有未提交的修改。你把 *.login 放到了 .gitignore 文件裏,可文件仍是在代碼庫裏 — 你怎麼才能告訴 Git 「撤銷」 對這個文件的追蹤呢?

方法: git rm --cached application.log

原理: 雖然 .gitignore 會阻止 Git 追蹤文件的修改,甚至不關注文件是否存在,但這只是針對那些之前歷來沒有追蹤過的文件。一旦有個文件被加入並提交了,Git 就會持續關注該文件的改變。相似地,若是你利用 git add -f 來強制或覆蓋了 .gitignore, Git 還會持續追蹤改變的狀況。以後你就沒必要用-f  來添加這個文件了。

若是你但願從 Git 的追蹤對象中刪除那個本應忽略的文件, git rm --cached 會從追蹤對象中刪除它,但讓文件在磁盤上保持原封不動。由於如今它已經被忽略了,你在  git status 裏就不會再看見這個文件,也不會再偶然提交該文件的修改了。

 

這就是如何在 Git 裏撤銷任何操做的方法。要了解更多關於本文中用到的 Git 命令,請查看下面的有關文檔:

相關文章
相關標籤/搜索