Skip to content
This page has been auto-translated and may contain errors.View in English

在 Git 中撤销更改和提交

docs.scrimba.com

总有些东西需要撤销。你输入了一个无意中的编辑,尝试的修复使情况更糟,或者提交了一些你希望没人看到的内容。Git 保存了它曾见过的项目几乎每个版本的记录,所以大多数错误的可恢复性比当时感觉到的要强。你需要哪个工具取决于错误的传播程度:仍然在编辑器中、已经暂存、已经提交,或者已经推送到团队成员可以看到的地方。本章按这个顺序逐一介绍每个阶段。

什么是推送和拉取?

推送将你的提交发送到团队成员也在工作的存储库的共享副本,拉取将他们的提交下载到你的机器。远程和 GitHub 完整覆盖了这两者;在本章中,"推送"意味着提交已离开你的机器。

在提交前丢弃更改

假设你仍在 提交循环 中的 weather-app 里重新设计 styles.css,但新布局被证明是个坏主意。你还没有暂存它,也没有提交它。你想让文件恢复到之前的样子,仅此而已。恰好完成这项工作的命令是 git restore

bash
$ 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 clean

git restore <file> 丢弃未提交的编辑,并将文件恢复到上次保存的状态。如果你已经用 git add 暂存了更改,但后来又改变了主意,那么普通的 git restore 不会触及它,因为文件现在位于暂存区而不仅仅在你的工作目录中。先将其从暂存区移出,然后恢复:

bash
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.css

两个分离的操作用于两个分离的区域:git restore --staged 将文件从暂存区移出,单独使用 git restore 丢弃工作目录中的编辑。git restore 仅适用于未提交的工作,一旦提交了更改,就需要使用不同的命令。

较早的教程和脚本使用 git checkout -- <file> 来完成 git restore <file> 现在所做的工作。checkout 过去同时用作"切换分支"和"恢复文件",这造成了足够的混淆,所以 Git 将这两个功能拆分为 switchrestore。你仍然会在旧的资料中看到 checkout,可以识别它,但在你现在编写的任何东西中应该使用 restore。你也可以用 git restore . 一次性丢弃当前文件夹中的所有未暂存更改,这值得在运行前停顿,因为它一次清除所有文件。

restore 的撤销严格停留在工作目录。提交是一个保存的对象,在 Git 的垃圾回收移除它之前一直保留,这就是为什么坏的提交即使在硬重置后仍可通过 reflog 恢复。未提交的编辑根本不会变成保存的对象:restore 从编辑上检查出文件的最后保存版本,没有早期状态保存在任何地方供挖掘。这就是及早并频繁提交的实际案例。一个混乱的提交几乎总是可修复的;你从未保存的编辑通常不是。

Juno丢弃未提交的更改git restore <file> 丢弃你还未提交的编辑,并将文件恢复到之前的样子。如果你已经用 git add 暂存了更改,先用 git restore --staged <file> 将其从暂存区移出,然后如果你想让编辑消失,再恢复文件。这仅适用于你还未提交的更改,所以它无法抢救你已保存的错误。
Juno丢弃未提交的更改git restore <file>git checkout -- <file> 的现代替代品,而 git restore . 一次清除文件夹中的所有未暂存更改,所以先运行 git status 查看会影响什么。你仍然会在较早的答案和脚本中看到 checkout 执行此工作,这是相同的想法,只是使用了一个较早的、更重载的名称。
Juno丢弃未提交的更改restore 从不创建或触及提交,它只用文件的最后保存版本覆盖你的工作目录。这意味着如果你犯了错误,它无法留下恢复的线索,不像本章中几乎所有其他东西。频繁提交且小幅提交,"哎呀"就变成了 reset 或 reflog 可以修复的问题,而不是从未有保存点返回的更改。

用 git stash 暂存工作

有时没有错误要修复,只是时机不好。你正在编辑 styles.css,一个团队成员现在需要你转向另一个分支,但你还没有准备好提交半成品的样式。git stash 暂存你的更改,给你一个干净的工作目录,这样你可以离开并稍后返回。

bash
$ 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 把它恢复:

bash
$ git stash pop
On branch main
Changes not staged for commit:
        modified:   styles.css

git stash 用于你还未提交的更改,它从不在你的分支上创建提交。

单个 stash 很少涵盖真实一周的工作,所以要了解其他的工具包。git stash list 显示你暂存的所有内容,最近的优先。git stash pop 重新应用最顶部的条目并将其从列表中移除;git stash apply 重新应用它但将其留在列表中,如果你想在两个分支上使用相同的暂存更改时很有用。在输入时为 stash 命名,这样 list 后来不会是一堵未标记的条目墙:

bash
$ git stash push -m "wip: forecast card layout"
$ git stash list
stash@{0}: On main: wip: forecast card layout

stash 是由与 Git 中所有其他东西相同的提交机制构建的。git stash 将你的暂存和未暂存状态保存为几个提交对象,并指向一个特殊引用 refs/stash,这就是为什么 git stash list 像自己的一个小历史。reflog 依赖相同的技巧,一个指向没有分支到达的提交的引用。

Juno暂存工作git stash 暂存你还未提交的更改,所以你的工作目录变干净,git stash pop 稍后把它们恢复回来。没有东西被提交,也没有东西丢失,编辑只是停靠了一会。我每当团队成员现在需要我转向另一个分支时就频繁使用这个。
Juno暂存工作git stash list 显示每个暂存的更改,pop 重新应用并移除最近的一个,apply 重新应用而不移除它。在输入时用 push -m 为 stash 命名,一周后充满未标记条目的 stash 列表对任何人都没有帮助,包括你。
Juno暂存工作 stash 是几个提交对象,refs/stash 指向它们,坐在任何分支之外,这就是为什么它的行为像自己的小历史。一旦你看到它这样,stash 就不再感觉像魔法,它是相同的对象模型再次做相同的技巧。

用 git revert 撤销提交

假设提交 8f3c7d1,"Cache forecast responses in localStorage",被证明是错误的,并且它已经被推送,所以一个团队成员已经拉取了它。现在重写该提交会改变别人已经拥有的东西,他们的副本和你的副本在下一次拉取时会不一致。git revert 解决了这个问题而不改变任何已经存在的东西:它创建一个全新的提交,应用变化的反面。

bash
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"

历史仍然显示两个提交,原始的和撤销它的,按顺序。没有人的副本需要重新塑造,团队成员像拉取任何其他提交一样拉取新的恢复提交。

git revert 在别人已经拥有的提交上总是安全的,因为它添加新提交而不是改变旧的。

恢复与后来提交重叠的更改可能会产生冲突,这与 合并和冲突 覆盖的相同类型:Git 用 <<<<<<<=======>>>>>>> 标记文件并等待你解决它。修复标记,git add 文件,然后用 git revert --continue 完成:

bash
$ git revert 8f3c7d1
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js

$ git add src/index.js
$ git revert --continue

恢复在共享历史上的安全性基于与从不对公共分支进行变基相同的想法:重写别人已经拉取的提交会使他们的副本和你的副本不同步,之后需要有人强行绕过不匹配。恢复让每个现有提交完全保持原样。它只添加一个新的,所以没有人的副本需要与你的调和。

Juno用恢复撤销提交git revert <commit> 通过添加一个全新的提交来撤销一个提交,该提交反转它,而不是删除原始的。这使其成为一旦提交已经与任何其他人共享时的安全选择,因为没有已保存的东西被改变。历史记录保留了错误和修复的记录,我使用 Git 的时间越长就越喜欢这样。
Juno用恢复撤销提交 恢复可能像合并一样冲突,当后来提交触及相同行时,用相同的方式解决它:修复标记,add 文件,然后 git revert --continue。每当你正在撤销的提交已经推送或共享时使用它。
Juno用恢复撤销提交 恢复从不触及现有提交,所以它是与已经其他人正在拉取的分支配合良好的撤销工具。一旦坏提交已离开你自己的机器,就将其作为你的默认选择。

用 git reset 回卷:soft、mixed 和 hard

有第三种方式来撤销提交,git reset,它的工作方式与恢复不同:reset 不是添加一个撤销旧提交的新提交,而是移动你当前分支以指向完全不同的提交。这使它对没有人看到的提交快速而干净,对他们已看到的提交则有风险。

Reset 是恢复和恢复上面的更宽泛的、更容易误用的表亲。大多数日常修复中,你不需要它,恢复、恢复和暂存涵盖了绝大多数"帮助,撤销这个"的时刻。你将在其他人的说明中看到它,所以这里是概括:reset 在历史中向后移动你的分支,它的一个模式 --hard,毫无确认地丢弃未提交的工作。如果你从某处复制的命令包括 git reset --hard,在运行前读一下它做什么。

git reset <commit> 将你的分支移至指向该提交,--soft--mixed--hard 决定还有多少其他东西随之移动。想象三层:你的提交历史,分支指针所在的位置,暂存区,什么排队待提交,以及你的工作目录,磁盘上的文件。下面的短哈希值的读取方式与 历史git log --oneline 覆盖的相同,目标 HEAD~1 意味着"在 HEAD 之前的一个提交",说"撤销最新提交"的标准方式。

bash
$ git log --oneline
8f3c7d1 Cache forecast responses in localStorage
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
5f3d8b2 Add five-day forecast to the dashboard
1a2b3c4 Set up project structure

$ git reset --soft HEAD~1

--soft 将分支指针向后移动一个提交然后停止。暂存区和工作目录未被触及,所以已撤销提交中的所有内容都坐在暂存中,准备以你喜欢的任何方式重新提交。

bash
$ git reset --mixed HEAD~1

--mixed 是如果你省略标志会运行的。它将分支指针向后移动,也清除暂存区,所以撤销提交的更改作为未暂存的编辑回到你的工作目录。没有东西丢失,在提交前再次运行 git add

bash
$ git reset --hard HEAD~1

git reset --hard 将分支指针向后移动并覆盖你的工作目录以匹配,所以任何未提交的工作和撤销的提交都在一步内从视图中消失,没有提示问你是否确定。这是运行前要停顿的模式。

这也解释了 git commit --amend提交循环 中做什么:amend 几乎接近 reset --soft HEAD~1 然后立即跟随一个新提交,这就是为什么它替换你的最后提交而不是堆叠在它之上。

Reset 仅仅移动指针,在 --hard 模式中,工作目录以匹配。它从不彻底删除提交对象。reset --hard 遗留的提交对象仍然坐在 Git 的对象数据库中,无法从任何分支到达,这很重要,因为有办法再次到达它:git reflog,接下来。在共享分支上,将你的分支向后移动并推送意味着强制推送覆盖你的队友已经拥有的提交,所以他们下一次拉取与重新编写的历史不一致。这就是 reset 保持为本地清理工具的实际原因,而恢复覆盖已经离开你的机器的任何东西。

Juno用 git reset 回卷 在大多数日常修复中,你不会使用 git reset,恢复、恢复和暂存已经覆盖了几乎所有东西。仍然,知道它的形状:reset 将你的分支向后移动到较早的提交,它的 --hard 模式毫无确认地擦除你的工作目录以匹配。对你复制包括 reset --hard 的任何命令要真正谨慎。
Juno用 git reset 回卷git reset --soft 保持撤销提交的更改暂存,--mixed(默认)取消暂存但保持编辑,--hard 彻底丢弃编辑。commit --amend 基本上是一个软重置紧接着一个新提交,这就是为什么它替换最后提交而不是堆叠在它之上。
Juno用 git reset 回卷 Reset 仅移动指针,用 --hard 时,你的工作目录以匹配,它从不删除提交对象本身。该对象在数据库中保留直到 reflog 和垃圾回收跟上它。推送重新编写的分支,你的队友下一次拉取与不再匹配他们的历史冲突。

选择恢复或重置

决定使用哪个工具的规则:别人已经拉取这个提交了吗?如果是,使用恢复。如果提交只存在于你的机器上,在一个你还未推送的分支上,reset 是可以的,通常是更整洁的修复。当你不确定时,恢复总是安全选择,因为它适用于两种情况。

"别人拉取了吗"真正意味着"这个提交是否可从别人拥有的引用到达",实际上意味着它是否坐在已被推送的分支上。五分钟前发明的本地分支上的提交,从未推送过,对于 reset 是开放领地。你推送它的那一刻,就把它当作共享:在本地重置它并强制推送意味着首先与触及该分支的其他任何人协调。

Juno选择恢复或重置 在撤销提交前问一个问题:别人已经拉取了吗?如果是,使用恢复,它总是安全的。如果提交只存在于你的机器上,reset 也可以。当你不确定哪个适用时,恢复是更安全的默认。
Juno选择恢复或重置 一旦提交被推送,任何人都可能拥有它,就使用恢复。虽然它仍然完全是本地的,reset 是可以的,也经常是更整洁的修复,因为它不会在日志中留下"恢复恢复"的条目。
Juno选择恢复或重置 真正的测试是可达性:这个提交是否坐在别人已拉取的引用上?本地且未推送,reset 是开放领地。推送且共享,将重置它当作需要首先协调的东西对待,因为以后的强制推送与你的队友已经拥有的东西冲突。

用 git reflog 恢复提交

reset --hard、凌乱的变基或误删分支看起来像提交完全消失了。它通常没有。Git 保持了一个本地日志,记录你的 HEAD 和分支所指向的所有位置,称为 reflog,而且该日志几乎总是你回到看起来丢失的提交的方式。

你不太可能经常需要这个,但这是概括:Git 很少在看起来有东西的时候就立即丢弃。如果你曾运行过 reset --hard 然后意识到你之后需要那个提交,很可能有办法让它回来,当你准备更深入去时这里有覆盖。

简短版本:git reflog 列出 HEAD 的最近位置,每个都有短哈希,你可以 git reset --hard 任何这些哈希以回到那个确切状态。它像第二个历史:HEAD 站过的每个地方,包括你的分支不再指向的提交。

一个没有分支、标签或 HEAD 再指向它的提交是 无法到达的:在存储库中漂流,仍然保存,只是缺少一个标签导回它。reflog 是你的机器上 HEAD 移动处的时间顺序列表,这个分支切换、那个重置、那个变基,每个条目用短引用 HEAD@{2} 之类的键控。找到错误之前的条目,指回它的分支:

bash
$ git reflog
2b6f0d1 HEAD@{0}: reset: moving to HEAD~1
8f3c7d1 HEAD@{1}: commit: Cache forecast responses in localStorage
4f2a1c9 HEAD@{2}: commit: Fix temperature conversion in Celsius-to-Fahrenheit formula

$ git reset --hard 8f3c7d1

那就是 reset --hard 看起来抹去的提交,已恢复。两个值得知道的限制。首先,reflog 仅存在于你自己的机器上,推送和拉取从不触及它,所以它无法恢复团队成员在他们的机器上丢失的提交。其次,它不会永久保留:Git 最终运行垃圾回收,一旦无法到达的提交超过几周的默认截止日期就清除它们,所以安全网覆盖最近的错误而不是项目的整个生命周期。

Juno恢复丢失的提交 如果你曾运行 reset --hard 然后太迟意识到你需要那个提交,它可能仍然可恢复。Git 保持一个称为 reflog 的本地记录,记录你的工作所有指向的地方,这通常足以让一个看起来消失的提交回来。这是深入领地,但知道它在那里是令人放心的。
Juno恢复丢失的提交git reflog 列出 HEAD 的最近位置,带有短哈希你可以 reset --hard 回到,所以一个走得太远的重置通常是可恢复的。但它仅存在于你自己的机器上,所以它无法拯救团队成员在他们的上丢失的提交。
Juno恢复丢失的提交 reflog 是本地的、HEAD 站过每个地方的时间顺序记录,这就是 reset --hard 看起来抹去的提交通过指向正确的 HEAD@{n} 条目的分支再次出现的方式。它最终与垃圾回收老化,它仅覆盖你自己最近的错误。一个丢失提交的团队成员需要在他们自己的机器上运行相同的恢复。