天天看點

如何在 Git 裡撤銷(幾乎)任何操作

任何版本控制系統的一個最有的用特性就是“撤銷 (undo)”你的錯誤操作的能力。在 git 裡,“撤銷” 蘊含了不少略有差别的功能。

當你進行一次新的送出的時候,git 會儲存你代碼庫在那個特定時間點的快照;之後,你可以利用 git 傳回到你的項目的一個早期版本。

在本篇博文裡,我會講解某些你需要“撤銷”已做出的修改的常見場景,以及利用 git 進行這些操作的最佳方法。

如何在 Git 裡撤銷(幾乎)任何操作

場景: 你已經執行了 <code>git push</code>, 把你的修改發送到了 github,現在你意識到這些 commit 的其中一個是有問題的,你需要撤銷那一個 commit.

方法: <code>git revert &lt;sha&gt;</code>

原理: <code>git revert</code> 會産生一個新的 commit,它和指定 sha 對應的 commit 是相反的(或者說是反轉的)。如果原先的 commit 是“物質”,新的 commit 就是“反物質” — 任何從原先的 commit 裡删除的内容會在新的 commit 裡被加回去,任何在原先的 commit 裡加入的内容會在新的 commit  裡被删除。

這是 git 最安全、最基本的撤銷場景,因為它并不會改變曆史 — 是以你現在可以  <code>git push </code>新的“反轉” commit 來抵消你錯誤送出的 commit。

<a target="_blank"></a>

場景: 你在最後一條 commit 消息裡有個筆誤,已經執行了 <code>git commit -m "fxies bug #42"</code>,但在 <code>git push</code> 之前你意識到消息應該是 “fixes bug #42″。

方法: <code>git commit --amend</code> 或 <code>git commit --amend -m "fixes bug #42"</code>

原理: <code>git commit --amend</code> 會用一個新的 commit 更新并替換最近的 commit ,這個新的 commit 會把任何修改内容和上一個 commit 的内容結合起來。如果目前沒有提出任何修改,這個操作就隻會把上次的 commit 消息重寫一遍。 

場景: 一隻貓從鍵盤上走過,無意中儲存了修改,然後破壞了編輯器。不過,你還沒有 commit 這些修改。你想要恢複被修改檔案裡的所有内容 — 就像上次 commit 的時候一模一樣。

方法: <code>git checkout -- &lt;bad filename&gt;</code>

原理: <code>git checkout</code> 會把工作目錄裡的檔案修改到 git 之前記錄的某個狀态。你可以提供一個你想傳回的分支名或特定 sha ,或者在預設情況下,git 會認為你希望 checkout 的是 <code>head</code>,目前 checkout 分支的最後一次 commit。

記住:你用這種方法“撤銷”的任何修改真的會完全消失。因為它們從來沒有被送出過,是以之後 git 也無法幫助我們恢複它們。你要確定自己了解你在這個操作裡扔掉的東西是什麼!(也許可以先利用 <code>git diff</code> 确認一下)

場景: 你在本地送出了一些東西(還沒有 push),但是所有這些東西都很糟糕,你希望撤銷前面的三次送出 — 就像它們從來沒有發生過一樣。

方法: <code>git reset &lt;last good sha&gt;</code> 或 <code>git reset --hard &lt;last good sha&gt;</code>

原理: <code>git reset</code> 會把你的代碼庫曆史傳回到指定的 sha 狀态。 這樣就像是這些送出從來沒有發生過。預設情況下, <code>git reset</code> 會保留工作目錄。這樣,送出是沒有了,但是修改内容還在磁盤上。這是一種安全的選擇,但通常我們會希望一步就“撤銷”送出以及修改内容 — 這就是 <code>--hard</code> 選項的功能。

場景: 你送出了幾個 commit,然後用 <code>git reset --hard</code> 撤銷了這些修改(見上一段),接着你又意識到:你希望還原這些修改!

方法: <code>git reflog</code> 和 <code>git reset</code> 或 <code>git checkout</code>

原理: <code>git reflog</code> 對于恢複項目曆史是一個超棒的資源。你可以恢複幾乎 任何東西 — 任何你 commit 過的東西 — 隻要通過 reflog。

你可能已經熟悉了 <code>git log</code> 指令,它會顯示 commit 的清單。 <code>git reflog</code> 也是類似的,不過它顯示的是一個 <code>head</code> 發生改變的時間清單.

一些注意事項:

<code>它涉及的隻是 head</code> 的改變。在你切換分支、用 <code>git commit</code> 進行送出、以及用 <code>git reset</code> 撤銷 commit 時,<code>head</code> 會改變,但當你用  <code>git checkout -- &lt;bad filename&gt;</code> 撤銷時(正如我們在前面講到的情況),head 并不會改變 — 如前所述,這些修改從來沒有被送出過,是以 reflog 也無法幫助我們恢複它們。

<code>git reflog</code> 不會永遠保持。git 會定期清理那些 “用不到的” 對象。不要指望幾個月前的送出還一直躺在那裡。

你的 <code>reflog</code> 就是你的,隻是你的。你不能用 <code>git reflog</code> 來恢複另一個開發者沒有 push 過的 commit。

如何在 Git 裡撤銷(幾乎)任何操作

reflog

那麼…你怎麼利用 reflog 來“恢複”之前“撤銷”的 commit 呢?它取決于你想做到的到底是什麼:

如果你希望準确地恢複項目的曆史到某個時間點,用 <code>git reset --hard &lt;sha&gt;</code>

如果你希望重建工作目錄裡的一個或多個檔案,讓它們恢複到某個時間點的狀态,用 <code>git checkout &lt;sha&gt; -- &lt;filename&gt;</code>

如果你希望把這些 commit 裡的某一個重新送出到你的代碼庫裡,用 <code>git cherry-pick &lt;sha&gt;</code>

場景: 你進行了一些送出,然後意識到你開始 check out 的是 <code>master</code> 分支。你希望這些送出進到另一個特性(feature)分支裡。

方法: <code>git branch feature</code>, <code>git reset --hard origin/master</code>, and <code>git checkout feature</code>

原理: 你可能習慣了用 <code>git checkout -b &lt;name&gt;</code> 建立新的分支 — 這是建立新分支并馬上 check out 的流行捷徑 — 但是你不希望馬上切換分支。這裡, <code>git branch feature</code> 建立一個叫做 <code>feature</code> 的新分支并指向你最近的 commit,但還是讓你 check out 在 <code>master 分支上。</code>

下一步,在送出任何新的 commit 之前,用 <code>git reset --hard</code> 把 <code>master</code> 分支倒回 <code>origin/master</code> 。不過别擔心,那些 commit 還在 <code>feature</code> 分支裡。

最後,用 <code>git checkout</code> 切換到新的 <code>feature</code> 分支,并且讓你最近所有的工作成果都完好無損。

場景: 你在 master 分支的基礎上建立了 <code>feature</code> 分支,但 <code>master</code> 分支已經滞後于 <code>origin/master</code> 很多。現在 <code>master</code> 分支已經和 <code>origin/master</code> 同步,你希望在 <code>feature</code> 上的送出是從現在開始,而不是也從滞後很多的地方開始。

方法: <code>git checkout feature</code> 和 <code>git rebase master</code>

原理: 要達到這個效果,你本來可以通過 <code>git reset</code> (不加 <code>--hard</code>, 這樣可以在磁盤上保留修改) 和 <code>git checkout -b &lt;new branch name&gt;</code> 然後再重新送出修改,不過這樣做的話,你就會失去送出曆史。我們有更好的辦法。

<code>git rebase master</code> 會做如下的事情:

首先它會找到你目前 check out 的分支和 <code>master</code> 分支的共同祖先。

然後它 reset 目前  check out 的分支到那個共同祖先,在一個臨時儲存區存放所有之前的送出。

然後它把目前 check out 的分支提到 <code>master</code> 的末尾部分,并從臨時儲存區重新把存放的 commit 送出到 <code>master</code> 分支的最後一個 commit 之後。

場景: 你向某個方向開始實作一個特性,但是半路你意識到另一個方案更好。你已經進行了十幾次送出,但你現在隻需要其中的一部分。你希望其他不需要的送出統統消失。

方法: <code>git rebase -i &lt;earlier sha&gt;</code>

原理: <code>-i</code> 參數讓 <code>rebase</code> 進入“互動模式”。它開始類似于前面讨論的 rebase,但在重新進行任何送出之前,它會暫停下來并允許你詳細地修改每個送出。

<code>rebase -i</code> 會打開你的預設文本編輯器,裡面列出候選的送出。如下所示:

如何在 Git 裡撤銷(幾乎)任何操作

rebase-interactive1

前面兩列是鍵:第一個是標明的指令,對應第二列裡的 sha 确定的 commit。預設情況下, <code>rebase -i</code>  假定每個 commit 都要通過  <code>pick</code> 指令被運用。

要丢棄一個 commit,隻要在編輯器裡删除那一行就行了。如果你不再需要項目裡的那幾個錯誤的送出,你可以删除上例中的1、3、4行。

如果你需要保留 commit 的内容,而是對 commit 消息進行編輯,你可以使用 <code>reword</code> 指令。 把第一列裡的 <code>pick</code> 替換為 <code>reword</code> (或者直接用 <code>r</code>)。有人會覺得在這裡直接重寫 commit 消息就行了,但是這樣不管用 —<code>rebase -i</code> 會忽略 sha 列前面的任何東西。它後面的文本隻是用來幫助我們記住 <code>0835fe2</code> 是幹啥的。當你完成 <code>rebase -i</code> 的操作之後,你會被提示輸入需要編寫的任何 commit 消息。

如果你需要把兩個 commit 合并到一起,你可以使用 <code>squash</code> 或 <code>fixup</code> 指令,如下所示:

如何在 Git 裡撤銷(幾乎)任何操作

rebase-interactive2

<code>squash</code> 和 <code>fixup</code> 會“向上”合并 — 帶有這兩個指令的 commit 會被合并到它的前一個 commit 裡。在這個例子裡, <code>0835fe2</code> 和 <code>6943e85</code> 會被合并成一個 commit, <code>38f5e4e</code> 和 <code>af67f82</code> 會被合并成另一個。

如果你選擇了 <code>squash,</code> git 會提示我們給新合并的 commit 一個新的 commit 消息; <code>fixup</code> 則會把合并清單裡第一個 commit 的消息直接給新合并的 commit 。 這裡,你知道 <code>af67f82</code> 是一個“完了完了….” 的 commit,是以你會留着 <code>38f5e4e</code> 的 commit 消息,但你會給合并了 <code>0835fe2</code> 和 <code>6943e85</code> 的新 commit 編寫一個新的消息。

在你儲存并退出編輯器的時候,git 會按從頂部到底部的順序運用你的 commit。你可以通過在儲存前修改 commit 順序來改變運用的順序。如果你願意,你也可以通過如下安排把 <code>af67f82</code> 和 <code>0835fe2</code> 合并到一起:

如何在 Git 裡撤銷(幾乎)任何操作

rebase-interactive3

場景: 你在一個更早期的 commit 裡忘記了加入一個檔案,如果更早的 commit 能包含這個忘記的檔案就太棒了。你還沒有 push,但這個 commit 不是最近的,是以你沒法用 <code>commit --amend</code>.

方法: <code>git commit --squash &lt;sha of the earlier commit&gt;</code> 和 <code>git rebase --autosquash -i &lt;even earlier sha&gt;</code>

原理: <code>git commit --squash</code> 會建立一個新的 commit ,它帶有一個 commit 消息,類似于 <code>squash! earlier commit</code>。 (你也可以手工建立一個帶有類似 commit 消息的 commit,但是 <code>commit --squash</code> 可以幫你省下輸入的工作。)

如果你不想被提示為新合并的 commit 輸入一條新的 commit 消息,你也可以利用 <code>git commit --fixup</code> 。在這個情況下,你很可能會用<code>commit --fixup</code> ,因為你隻是希望在 <code>rebase</code> 的時候使用早期 commit 的 commit 消息。

<code>rebase --autosquash -i</code>  會激活一個互動式的 <code>rebase</code> 編輯器,但是編輯器打開的時候,在 commit 清單裡任何 <code>squash!</code> 和 <code>fixup!</code> 的 commit 都已經配對到目标 commit 上了,如下所示:

如何在 Git 裡撤銷(幾乎)任何操作

rebase-autosquash

在使用 <code>--squash</code> 和 <code>--fixup</code> 的時候,你可能不記得想要修正的 commit 的 sha 了— 隻記得它是前面第 1 個或第 5 個 commit。你會發現 git 的 <code>^</code> 和 <code>~ </code>操作符特别好用。<code>head^</code> 是 <code>head </code>的前一個 commit。 <code>head~4</code> 是 <code>head</code> 往前第 4 個 – 或者一起算,倒數第 5 個 commit。

場景: 你偶然把 <code>application.log</code> 加到代碼庫裡了,現在每次你運作應用,git 都會報告在 <code>application.log</code> 裡有未送出的修改。你把 <code>*.log</code>in 放到了 <code>.gitignore</code> 檔案裡,可檔案還是在代碼庫裡 — 你怎麼才能告訴 git “撤銷” 對這個檔案的追蹤呢?

方法: <code>git rm --cached application.log</code>

原理: 雖然 <code>.gitignore</code> 會阻止 git 追蹤檔案的修改,甚至不關注檔案是否存在,但這隻是針對那些以前從來沒有追蹤過的檔案。一旦有個檔案被加入并送出了,git 就會持續關注該檔案的改變。類似地,如果你利用 <code>git add -f</code> 來強制或覆寫了 <code>.gitignore</code>, git 還會持續追蹤改變的情況。之後你就不必用<code>-f</code>  來添加這個檔案了。

如果你希望從 git 的追蹤對象中删除那個本應忽略的檔案, <code>git rm --cached</code> 會從追蹤對象中删除它,但讓檔案在磁盤上保持原封不動。因為現在它已經被忽略了,你在  <code>git status</code> 裡就不會再看見這個檔案,也不會再偶然送出該檔案的修改了。

這就是如何在 git 裡撤銷任何操作的方法。要了解更多關于本文中用到的 git 指令,請檢視下面的有關文檔:

<a href="http://git-scm.com/docs/git-checkout" target="_blank">checkout</a>

<a href="http://git-scm.com/docs/git-commit" target="_blank">commit</a>

<a href="http://git-scm.com/docs/git-rebase" target="_blank">rebase</a>

<a href="http://git-scm.com/docs/git-reflog" target="_blank">reflog</a>

<a href="http://git-scm.com/docs/git-reset" target="_blank">reset</a>

<a href="http://git-scm.com/docs/git-revert" target="_blank">revert</a>

<a href="http://git-scm.com/docs/git-rm" target="_blank">rm</a>

<b>原文釋出時間為:2015-06-29</b>

<b></b>

<b>本文來自雲栖社群合作夥伴“linux中國”</b>

繼續閱讀