|
| 1 | +>原文:[How to undo (almost) anything with Git](https://github.blog/2015-06-08-how-to-undo-almost-anything-with-git/) |
| 2 | +> |
| 3 | +>译者:[madneal](https://github.com/madneal) |
| 4 | +> |
| 5 | +>welcome to star my [articles-translator](https://github.com/madneal/articles-translator/), providing you advanced articles translation. Any suggestion, please issue or contact [me](mailto:bing@stu.ecnu.edu.cn) |
| 6 | +> |
| 7 | +>LICENSE: [MIT](https://opensource.org/licenses/MIT) |
| 8 | +
|
| 9 | + |
| 10 | + |
| 11 | +任何版本控制系统最有用的功能之一就是能够“撤消”错误。在 Git 中,“撤消”可能意味着许多略有不同的事情。 |
| 12 | + |
| 13 | +当你进行新的 commit 时,Git 会及时存储你的仓库在该特定时刻的快照;之后,你可以使用 Git 返回到项目的早期版本。 |
| 14 | + |
| 15 | +在这篇文章中,我将介绍一些你可能想要“撤消”所做更改的常见场景,以及使用 Git 执行此操作的最佳方法。 |
| 16 | + |
| 17 | +## 撤销一个“public”修改 |
| 18 | + |
| 19 | +**场景:** 你刚刚运行了 `git push`,将你的修改 push 到 GitHub,现在意识到有一个 commit 有问题。你想把这个 commit 撤销。 |
| 20 | + |
| 21 | +**撤销:** `git revert <SHA>` |
| 22 | + |
| 23 | +**结果:** `git revert` 将创建一个与给定 SHA 相反的新 commit。如果旧 commit 是“matter”,则新 commit 是“anti-matter”——旧 commit 中删除的任何内容都将添加到新 commit 中,而旧 commit 中添加的任何内容都将在新 commit 中删除。 |
| 24 | + |
| 25 | +这是 Git 最安全、最基本的“撤消”场景,因为它不会更改历史记录,因此你现在可以使用 `git push` 来提交新的 commit来撤消错误的 commit。 |
| 26 | + |
| 27 | +## 修复上一个 commit message |
| 28 | + |
| 29 | +**场景:** 你刚刚打错了最新一条 commit message,你使用 `git commit -m "Fxies bug #41"`, 但在 `git push` 之前你意识到应该写 `Fixes bug #42`。 |
| 30 | + |
| 31 | +**撤销:** `git commit --amend` 或 `git commit --amend -m "修复 bug #42"` |
| 32 | + |
| 33 | +**结果:** `git commit --amend` 将更新最新 commit 并将其替换为新 commit ,将新 commit 与之前提交的 commit 的内容相结合。若当前没有任何 stage 内容,这只是重写了之前的 commit 消息。 |
| 34 | + |
| 35 | +## 撤销本地修改 |
| 36 | + |
| 37 | +**场景:** 猫走过键盘并以某种方式保存了更改,然后编辑器崩溃。不过,你还没有 commit 这些修改。你想要撤消该文件中的所有内容 - 只需返回到上次 commit 时的样子即可。 |
| 38 | + |
| 39 | +**撤消:** `git checkout -- <bad filename>` |
| 40 | + |
| 41 | +**结果:** `git checkout` 将工作目录中的文件更改为 Git 之前保存的状态。你可以提供要返回的分支名称或特定 `SHA`,或者默认情况下,Git 会假设你要切换到 `HEAD`,即当前分支上的最后一次 commit。 |
| 42 | + |
| 43 | +请记住:你以这种方式“撤消”的任何更改实际上都会消失。它们从未被 commit ,因此 Git 无法帮助我们稍后恢复它们。确保你知道你在这里扔掉了什么! (也许使用 `git diff` 来确认。) |
| 44 | + |
| 45 | +## 重置本地修改 |
| 46 | + |
| 47 | +**场景:** 你在本地进行了一些 commit (尚未 push),但一切都很糟糕,你想要撤消最后三个 commit - 就像它们从未发生过一样。 |
| 48 | + |
| 49 | +**撤消方式:** `git reset <last good SHA>` 或 `git reset --hard <last good SHA>` |
| 50 | + |
| 51 | +**结果:** `git reset` 将仓库的历史记录一直回溯到指定的 `SHA`。就好像这些 commit 从未发生过一样。默认情况下,`git reset` 保留工作目录。 commit 已消失,但内容仍在磁盘上。这是最安全的选择,但通常,你会希望一次“撤消” commit 和更改 - 这就是 `--hard` 参数的作用。 |
| 52 | + |
| 53 | +## 撤销本地修改后恢复 |
| 54 | + |
| 55 | +**场景:** 你进行了一些 commit ,执行了 `git reset --hard` 来“撤消”这些更改(见上文),然后意识到:你想要恢复这些更改! |
| 56 | + |
| 57 | +**撤消:** `git reflog` 和 `git reset` 或 `git checkout` |
| 58 | + |
| 59 | +**结果:** `git reflog` 是恢复项目历史记录的绝佳资源。你可以通过 reflog 恢复几乎任何内容(任何你 commit 的内容)。 |
| 60 | + |
| 61 | +你可能熟悉 `git log` 命令,它显示 commit 列表。 `git reflog` 类似,但显示 `HEAD` 更改的时间列表。 |
| 62 | + |
| 63 | +一些注意事项: |
| 64 | + |
| 65 | +* `HEAD` 只有在你切换分支时,使用 `git commit` 进行 commit 并使用 `git reset` 取消 commit 时,HEAD 会更改,但是当你 `git checkout -- <bad filename>` 时 `HEAD` 不会变化(来自较早的场景 - 如前所述,这些更改从未 commit ,因此 `reflog` 无法帮助我们恢复这些更改)。 |
| 66 | +* `git reflog` 不会一直有效。 Git 会定期清理“无法访问”的对象。不要指望在 `reflog` 一直发现几个月前的 commit 。 |
| 67 | +* 你的转发记录是你的,并且只属于你。你不能使用 `git reflog` 来恢复其他开发人员未 push 的 commit 。 |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +那么……如何使用 reflog 来“恢复”之前“撤消”的一个或多个 commit ?这取决于你到底想要完成什么: |
| 72 | + |
| 73 | +* 如果你想恢复项目当时的历史记录,请使用 `git reset --hard <SHA>` |
| 74 | +* 如果你想在工作目录中重新创建一个或多个文件,而不更改历史记录,请使用 `git checkout <SHA> -- <filename>` |
| 75 | +* 如果你想将其中一个 commit 重放到存储库中,请使用 `gitcherry-pick <SHA>` |
| 76 | + |
| 77 | +## 再一次,通过分支 |
| 78 | + |
| 79 | +**场景:** 你提交了一些 commit ,然后意识到你在 master 分支上。你希望可以在 `feature` 分支上提交 commit 。 |
| 80 | + |
| 81 | +**撤消:** `git branch feature`,`git reset --hard origin/master` 和 `git checkout feature` |
| 82 | + |
| 83 | +**结果:** 你可能习惯使用 `git checkout -b <name>` 创建新分支 - 这是创建新分支并立即 checkout 的流行快捷方式 - 但你不想立即切换到刚刚创建的分支。在这里,`git branch feature` 创建了一个名为 `feature` 的新分支,指向你最近的 commit ,但让你依然在 master 分支上。 |
| 84 | + |
| 85 | +接下来,在任何新 commit 之前, `git reset --hard` 将 `master` 回退到 `origin/master`。不过不用担心,它们仍然可以使用。 |
| 86 | + |
| 87 | +最后,`git checkout` 切换到新 `feature` 分支,你最近的所有工作都完好无损。 |
| 88 | + |
| 89 | +## 分支省时大法 |
| 90 | + |
| 91 | +**场景:** 你基于 `master` 分支创建了一个新的 `feature` 分支,但是 `master` 远远落后于 `origin/master`。现在 `master` 分支与 `origin/master` 同步,你希望 `feaute` 的 commit 现在就开始,而不是远远落后。 |
| 92 | + |
| 93 | +**撤消方式:** `git checkout feature` 和 `git rebase master` |
| 94 | + |
| 95 | +**结果:** 你可以使用 `git reset`(无 `--hard`,有意保留磁盘上的更改)然后 `git checkout -b <newbranch name>` 来完成此操作,然后重新 commit 更改,但这样,你会丢失 commit 历史记录。有一个更好的方法。 |
| 96 | + |
| 97 | + |
| 98 | +`git rebase master` 做了几件事: |
| 99 | + |
| 100 | +* 首先,它找到当前分支和 `master` 分支之间的共同祖先。 |
| 101 | +* 然后它将当前的分支重置为该祖先,将所有后续 commit 保存在保留区域中。 |
| 102 | +* 然后它将当前分支前放到 `master` 的末尾,并在 `master` 最后一次 commit 后重放保留区域的 commit 。 |
| 103 | + |
| 104 | +## 批量撤消/重做 |
| 105 | + |
| 106 | +**场景:** 你从一个方向开始功能开发,但在中途,你意识到另一种解决方案更好。你有十几个 commit ,但你只想要其中的一些,不想要其它的了。 |
| 107 | + |
| 108 | +**撤消:** `git rebase -i <earlier SHA>` |
| 109 | + |
| 110 | +**结果:** `-i` 将 `rebase` 置于“交互模式”。它像上面讨论的 rebase 一样开始,但在重放任何 commit 之前,它会暂停并允许你在重放时轻易修改每个 commit 。 |
| 111 | + |
| 112 | +`rebase -i` 将在默认文本编辑器中打开,并显示正在应用的 commit 列表,如下所示: |
| 113 | + |
| 114 | + |
| 115 | + |
| 116 | +前两列是关键:第一列是为第二列中的 SHA 标识的 commit 选择的命令。默认情况下,`rebase -i` 假设每个 commit 都使用 `pick` 命令。 |
| 117 | + |
| 118 | +要删除 commit ,只需在编辑器中删除该行即可。如果你不再希望项目中存在错误 commit ,则可以删除上面的第 1 行和第 3-4 行。 |
| 119 | + |
| 120 | +如果要保留 commit 的内容但编辑 commit 消息,可以使用 `reword` 命令。只需将第一列中的单词 `pick` 替换为单词 `reword` (或只是 `r`)。现在可能你觉得要重写 commit 消息,但这行不通—— `rebase -i` 会忽略 `SHA` 列之后的所有内容。之后的文字实际上只是为了帮助我们记住 `0835fe2` 的含义。当你完成 `rebase -i` 后,系统将提示你输入需要写入的任何新 commit 消息。 |
| 121 | + |
| 122 | +如果你想将两个 commit 合并在一起,你可以使用 `squash` 或 `fixup` 命令,如下所示: |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | +`squash` 和 `fixup` 向上合并 commit —— 使用这两个命令的 commit 将被合并到紧邻其之前的 commit 中。在这种情况下,`0835fe2` 和 `6943e85` 将合并为一个 commit ,然后 `38f5e4e` 和 `af67f82` 将合并为另一 commit 。 |
| 127 | + |
| 128 | +当你选择 `squash` 时,Git 会提示我们给新的组合 commit 一条新的 commit 消息;`fixup` 将为新 commit 提供列表中第一个 commit 的消息。在这里,你知道 `af67f82` 是一个“ooops” commit ,因此你只需按原样使用来自 `38f5e4e` 的 commit 消息,但你将为通过组合 `0835fe2` 和 `6943e85` 获得的新 commit 编写一条新消息。 |
| 129 | + |
| 130 | +当你保存并退出编辑器时,Git 将按从上到下的顺序应用你的 commit 。你可以通过在保存之前更改 commit 顺序来更改 commit 应用的顺序。如果你愿意,你可以通过如下安排将 `af67f82` 与 `0835fe2` 组合起来: |
| 131 | + |
| 132 | + |
| 133 | + |
| 134 | +## 修复较早的 commit |
| 135 | + |
| 136 | +**场景:** 你未能在早期 commit 中包含文件,如果早期 commit 能够以某种方式包含你遗漏的内容,那就太好了。你还没有 push ,但这不是最近的 commit ,所以你不能使用 `commit --amend`。 |
| 137 | + |
| 138 | +**撤销:** `git commit --squash <SHA of the earlier commit>` 和 `git rebase --autosquash -i <even earlier SHA>` |
| 139 | + |
| 140 | +**结果:** `git commit --squash` 将创建一个新的 commit ,其中包含类似 `squash! Earlier commit`。(你可以手动创建带有类似消息的 commit ,但 `commit --squash` 可以节省你的打字时间。) |
| 141 | + |
| 142 | +如果你不想提示你为组合 commit 编写新的 commit 消息,你也可以使用 `git commit --fixup`。在这种情况下,你可能会使用 `commit --fixup`,因为你只想在 rebase 期间使用早期 commit 的 commit 消息。 |
| 143 | + |
| 144 | +`rebase --autosquash -i` 将启动交互式 `rebase` 编辑器,但该编辑器将用任何 `sqush!` 和 `!fixup!` commit 已经与 commit 列表中的 commit 目标配对,如下所示: |
| 145 | + |
| 146 | + |
| 147 | + |
| 148 | +使用 `--squash` 和 `--fixup` 时,你可能不记得要修复的 commit 的 SHA,只记得它是一到五个 commit 前的。你可能会发现使用 Git 的 `^` 和 `~ `运算符特别方便。 `HEAD^` 是 HEAD 之前的一次 commit 。 `HEAD~4` 是 HEAD 之前的四次 commit ,或者总共是五次向后 commit 。 |
| 149 | +## Stop tracking a tracked file |
| 150 | + |
| 151 | +## 停止跟踪被跟踪的文件 |
| 152 | + |
| 153 | +**场景:** 你不小心将 `application.log` 添加到仓库中,现在每次运行应用程序时,Git 都会报告 `application.log` 中存在未暂存的更改。你将 `*.log` 放入 `.gitignore` 文件中,但它仍然存在 - 你如何告诉 git “撤消”跟踪此文件中的更改? |
| 154 | + |
| 155 | +**撤消:** `git rm --cached application.log` |
| 156 | + |
| 157 | +**结果:** 虽然 `.gitignore` 阻止 Git 跟踪文件的更改,甚至阻止它注意到以前从未跟踪过的文件的存在,但一旦添加并 commit 了文件,Git 将继续注意到该文件中的更改。同样,如果你使用 `git add -f` 来“强制”,或覆盖 `.gitignore`,Git 将继续跟踪更改。以后你不必使用 `-f`` 来添加它。 |
| 158 | + |
| 159 | +如果你想从 Git 的跟踪中删除那个应该被忽略的文件, `git rm --cached` 将从跟踪中删除它,但在磁盘上保留该文件不变。由于它现在被忽略,你将不会在 `git status` 中看到该文件,也不会意外地再次 commit 该文件的更改。 |
| 160 | + |
| 161 | +这就是使用 Git 撤消任何操作的方法。要了解有关此处使用的任何 Git 命令的更多信息,请查看相关文档: |
| 162 | + |
| 163 | +* [checkout](http://git-scm.com/docs/git-checkout) |
| 164 | +* [commit](http://git-scm.com/docs/git-commit) |
| 165 | +* [rebase](http://git-scm.com/docs/git-rebase) |
| 166 | +* [reflog](http://git-scm.com/docs/git-reflog) |
| 167 | +* [reset](http://git-scm.com/docs/git-reset) |
| 168 | +* [revert](http://git-scm.com/docs/git-revert) |
| 169 | +* [rm](http://git-scm.com/docs/git-rm) |
0 commit comments