撤销 Git 中的改动和提交


总有需要撤销的时候。你可能敲了一段不想要的改动,试了个反而把事情弄得更糟的修复方案,或者提交了不想让任何人看到的东西。Git 几乎记录了你项目曾经存在过的每一个版本,所以大多数错误都比它们看起来更容易挽回。用哪个工具补救,取决于这个错误走到了哪一步:还留在编辑器里、已经暂存、已经提交,还是已经推送到了队友能看见的地方。本章就按这个顺序依次讲解。
push 和 pull 是什么?
push 会把你的提交发送到一个队友们也在使用的共享仓库副本,pull 则会把队友的提交拉取到你本地。远程仓库与 GitHub 会完整讲解这两者;在本章里,"已推送"指的是这个提交已经离开了你的机器。
在提交之前丢弃一处改动
假设你还在 提交循环 那一节里改 weather-app 中的 styles.css 样式,结果发现新布局是个糟糕的主意。你还没有暂存它,也没有提交它。你只想把文件恢复成上一次保存时的样子,别的什么都不要。专门干这件事的命令就是 git restore:
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git restore styles.css
$ git status
On branch main
nothing to commit, working tree cleangit restore <file> 会丢弃一处尚未提交的编辑,把文件恢复成最后一次保存时的状态。如果你在改变主意之前已经用 git add 暂存了这处改动,单独的 git restore 就碰不到它了,因为这个文件现在待在暂存区,而不只是在你的工作目录里。这时得先把它移出暂存区,再恢复它:
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.css两个不同的操作对应两个不同的区域:git restore --staged 把文件从暂存区挪回去,单独的 git restore 则会丢弃工作目录里的编辑。git restore 只能触及尚未提交的工作,一旦改动被提交,就要换别的命令上场了。
git restore <file> 会丢弃一处你还没提交的编辑,把文件恢复成最后一次的样子。如果你已经用 git add 暂存了这个改动,先用 git restore --staged <file> 取消暂存,如果也想连编辑一起去掉,再恢复文件。这只对还没提交的改动有效,救不回你已经保存过的失误。 用 git stash 把工作暂时放到一边
有时候并没有什么错误要修,只是时机不凑巧。你正在改 styles.css 改到一半,队友现在就需要你切到另一个分支,可你还没准备好把改了一半的样式提交上去。git stash 会把你的改动收进一个"储藏室",交还给你一个干净的工作目录,让你可以先去忙别的事,之后再回来。
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git stash
Saved working directory and index state WIP on main: 8f3c7d1 Cache forecast responses in localStorage
$ git status
On branch main
nothing to commit, working tree clean对 styles.css 的编辑并没有消失,只是被存放起来了。等你准备好了,用 git stash pop 把它取回来:
$ git stash pop
On branch main
Changes not staged for commit:
modified: styles.cssgit stash 是给那些你还没准备好提交的改动用的,它绝对不会在你的分支上产生任何提交。
git stash 会把你还没准备好提交的改动收起来,让工作目录变干净,之后用 git stash pop 再取回来。既不会产生提交,也不会丢失任何东西,改动只是被暂时存放着而已。每次队友需要我立刻切到另一个分支时,我都会用这一招。 用 git revert 撤销一个提交
假设提交 8f3c7d1,也就是 "Cache forecast responses in localStorage",被发现是错的,而且它已经被推送出去了,队友也已经把它拉取下来了。如果现在改写这个提交,就会改变别人已经拥有的东西,等他们下次拉取时,他们的副本和你的就会对不上。git revert 能在不改变任何已有内容的前提下解决这个问题:它会创建一个全新的提交,把原来的改动反着做一遍。
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"历史记录里会同时保留这两个提交,原来那个和撤销它的那个,按顺序排列。没有人的副本需要被改动过的历史"重塑",队友只需要像拉取任何其他提交一样,拉取这个新的 revert 提交。
git revert 对别人已经拥有的提交永远是安全的,因为它是新增一个提交,而不是改动一个旧提交。
git revert <commit> 撤销一个提交的方式,是新增一个反向的提交,而不是抹掉原来那个。这让它成为一旦提交已经和别人共享之后的安全选择,因为任何已经保存下来的东西都不会被改动。历史记录会同时留下这个错误和修复它的记录,用 Git 用得越久,我越喜欢这一点。 用 git reset 倒回去:soft、mixed 和 hard
撤销一个提交还有第三种办法,git reset,它的做法和 revert 不一样:它不是新增一个提交来抵消旧的,而是把你当前分支直接指向另一个提交。这让它在处理还没有人见过的提交时又快又干净,但在处理别人已经见过的提交时就有风险。
reset 是前面 restore 和 revert 的"表亲",更强大,也更容易用错。在日常大多数修复场景里你用不到它,restore、revert 和 stash 已经覆盖了绝大多数"救命,把这个撤销一下"的时刻。不过你会在别人的操作说明里见到它,所以先记住要点:reset 会把你的分支往历史里倒回去,它的其中一种模式 --hard,会不加任何确认地丢弃未提交的工作。如果你从哪儿抄来的命令里带了 git reset --hard,运行之前先看清楚它到底会做什么。
git reset,restore、revert 和 stash 已经覆盖了几乎一切。不过还是要知道它的大致样子:reset 会把你的分支挪回到更早的一个提交,它的 --hard 模式会毫无确认地清空你的工作目录。凡是你抄来的命令里带了 reset --hard,都要格外小心对待。 该选 revert 还是 reset
决定用哪个工具的判断标准是:这个提交有没有被别人拉取过?如果有,用 revert。如果这个提交只存在于你自己的机器上,在一个你还没推送过的分支里,那么用 reset 没问题,往往还更干净利落。拿不准的时候,revert 永远是安全的选择,因为它在两种情况下都能用。
用 git reflog 找回一个提交
一次 reset --hard、一次搞砸的 rebase,或者不小心删掉了一个分支,看起来都像是某个提交彻底消失了。但通常并没有。Git 在本地保留了一份日志,记录着你的 HEAD 和各个分支曾经指向过的每一个地方,叫做 reflog,这份日志几乎总能带你找回一个看起来已经丢失的提交。
你大概率不会经常用到这个,但先记住要点:Git 很少会在东西看起来已经消失的那一刻,就真的把它扔掉。如果你哪次运行了 reset --hard,事后才发现那个提交其实还需要,多半是有办法找回来的,等你准备好深入了解的时候再看这一部分。
reset --hard,事后才发现那个提交其实还需要,那它多半还能找回来。Git 在本地保留了一份叫 reflog 的记录,记着你的工作曾经指向过的每一个地方,通常靠它就能把一个看起来已经消失的提交找回来。这部分内容属于 Deep Dive,但知道有这么个保险,会让人安心不少。 
