如何在 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 命令,请查看下面的有关文档: