天天看点

如何在 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>

继续阅读