在本节中,我们将讨论可用的“撤销”Git 策略和命令。首先要注意的是,Git 没有像文字处理应用中那样的传统“撤销”系统。避免将 Git 操作映射到任何传统的“撤销”心理模型将有所助益。此外,Git 有自己的“撤销”操作命名法,最好在讨论中加以利用。此命名法包括重置、恢复、签出、清理等术语。

一个有趣的比喻是将 Git 视为时间线管理工具。提交是项目历史记录时间线上某个时间点或兴趣点的快照。此外,可以通过使用分支来管理多个时间线。在 Git 中“撤销”时,通常会回到过去,或者回到另一个没有发生错误的时间线。

本教程提供了使用软件项目先前版本的所有必要技能。首先,它向您展示了如何浏览旧提交,然后解释了恢复项目历史记录中的公共提交与在本地计算机上重置未发布的变更之间的区别。

找出丢失的内容:查看旧提交

任何版本控制系统背后的整体想法都是存储项目的“安全”副本,这样您就不必担心对代码库造成不可挽回的破坏。构建项目的提交历史记录后,您便可以查看和重新访问历史记录中的任何提交。查看 Git 存储库历史记录的最佳实用程序之一是 git log 命令。在下面的示例中,我们使用 git log 获取一个热门的开源图片库获得的最新提交列表。

git log --oneline
e2f9a78fe Replaced FlyControls with OrbitControls
d35ce0178 Editor: Shortcuts panel Safari support.
9dbe8d0cf Editor: Sidebar.Controls to Sidebar.Settings.Shortcuts. Clean up.
05c5288fc Merge pull request #12612 from TyLindberg/editor-controls-panel
0d8b6e74b Merge pull request #12805 from harto/patch-1
23b20c22e Merge pull request #12801 from gam0022/improve-raymarching-example-v2
fe78029f1 Fix typo in documentation
7ce43c448 Merge pull request #12794 from WestLangley/dev-x
17452bb93 Merge pull request #12778 from OndrejSpanel/unitTestFixes
b5c1b5c70 Merge pull request #12799 from dhritzkiv/patch-21
1b48ff4d2 Updated builds.
88adbcdf6 WebVRManager: Clean up.
2720fbb08 Merge pull request #12803 from dmarcos/parentPoseObject
9ed629301 Check parent of poseObject instead of camera
219f3eb13 Update GLTFLoader.js
15f13bb3c Update GLTFLoader.js
6d9c22a3b Update uniforms only when onWindowResize
881b25b58 Update ProjectionMatrix on change aspect

每次提交都有一个唯一的 SHA-1 标识哈希。这些 ID 用于浏览已提交的时间线和重新访问提交。默认 git log 将仅显示当前选定分支的提交。您要找的提交完全有可能在另一个分支上。您可以通过执行 git log --branches=* 来查看所有分支的所有提交。git branch 命令用于查看和访问其他分支。调用该命令,git branch-a 将返回所有已知分支名称的列表。然后可以使用 git log 记录其中一个分支名称。

找到了指向您要访问的历史点的提交引用后,您可以使用 git checkout 命令来访问该提交。Git checkout 是将这些保存的快照中的任何一个“加载”到开发计算机的简单方法。在正常的开发过程中,HEAD 通常指向 main 分支或其他本地分支,但是当您签出旧提交时,HEAD 不再指向分支——它直接指向提交。这被称为“游离的 HEAD”状态,可以被看作如下所示:

Git 教程:查看之前的提交

签出旧文件不会移动 HEAD 指针。它将保留在同一个分支和提交中,从而避免出现“游离的 HEAD”状态。然后,您可以像提交任何其他更改一样,在新快照中提交旧版文件。因此,针对文件使用 git checkout 实际上是一种还原到单个文件的旧版本的方式。有关这两种模式的更多信息,请访问 git checkout 页面

查看旧修订版

这个例子假设您已经开始开发一个疯狂的实验,但您不确定自己是否想保留它。为了帮助您做出决定,您想在开始实验之前先看看项目的状态。首先,您需要找到自己想要查看的修订版本的 ID。

git log --oneline

假设您的项目历史记录如下所示:

b7119f2 Continue doing crazy things
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

您可以使用 git checkout 查看 “Make some import changes to hello.txt”按如下方式提交:

git checkout a1e8fb5

这使您的工作目录与 a1e8fb5 提交确切状态相匹配。您可以查看文件、编译项目、运行测试,甚至编辑文件,而不必担心丢失项目的当前状态。您在这里做的任何事情都不会保存在您的存储库中。要继续开发,您需要回到项目的“当前”状态:

git checkout main

此处假设您正在开发默认的 main 分支。返回 main 分支后,您可以使用 git revert git reset 来撤销任何不需要的更改。

撤销已提交的快照

从技术上讲,可通过几种不同的策略来“撤消”提交。下面的示例将假定我们的提交历史记录如下所示:

git log --oneline
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

我们将集中精力撤销 872fa7e 尝试一些疯狂的提交。也许事情有点太疯狂了。

如何使用 git checkout 撤销提交

使用 git checkout 命令,我们可以签出之前的提交 a1e8fb5,将存储库置于发生“疯狂”操作提交之前的状态。签出特定的提交会将存储库置于“游离的 HEAD”状态。这意味着您不再在任何分支上工作。在游离状态下,当您将分支更改回构建的分支时,所做的任何新提交都将被孤立。孤立的提交由 Git 的垃圾回收器删除。垃圾回收器以配置的时间间隔运行,并会永久销毁孤立的提交。为了防止孤立的提交被作为垃圾收集,我们需要确保位于一个分支上。

从分离的 HEAD 状态中,我们可以执行 git checkout -b new_branch_without_crazy_commit。这将创建一个名为 new_branch_without_crazy_cromit 的新分支并切换到该状态。该代码存储库现已进入新的历史记录时间线,其中 872fa7e 提交已不复存在。此时,我们可以继续研究这个新分支,其中 872fa7e 提交已不存在,并将其视为“撤销”。不幸的是,如果您需要之前的分支,也许它是您的 main 分支,这个撤销策略不合适。我们来看看其他一些“撤销”策略。有关更多信息和示例审查,请查看我们深入的 git checkout 讨论。

如何使用 git revert 撤销公开提交

假设我们回到最初的提交历史记录示例。该历史记录包含 872fa7e 提交。现在,让我们尝试还原“撤消”。如果我们执行 git revert HEAD,Git 将根据最后一次提交的反转创建一个新提交。这将向当前分支历史记录添加新的提交,此时类似于:

git log --oneline
e2f9a78 Revert "Try something crazy"
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

此时,我们在技术上再次“撤销”了 872fa7e 提交。尽管 872fa7e 在历史记录中仍然存在,但新的 e2f9a78 提交与 872fa7e 中的变更相反。与之前的签出策略不同,我们可以继续使用相同的分支。这个解决方案是一个令人满意的撤销。这是处理公共共享存储库的理想“撤销”方法。如果您需要保留精心策划的最少 Git 历史记录,那么这个策略可能无法令人满意。

如何使用 git reset 撤销提交

对于此撤消策略,我们将继续使用我们的工作示例。git reset 是一个具有多种用途和功能的广泛命令。如果我们调用 git reset --hard a1e8fb5,提交历史记录将重置为指定的提交。使用 git log 检查提交历史记录,此时记录将类似于:

git log --oneline
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

日志输出显示 e2f9a78872fa7e 提交已不存在于提交历史记录中。此时,我们可以继续工作并创建新提交,就好像“疯狂”的提交从未发生过一样。这种撤销变更的方法对历史记录的影响最为明显。重置对于本地变更非常有用,但是在使用共享远程存储库时会增加复杂性。如果我们有一个将 872fa7e 提交推送到它的共享远程存储库,然后我们尝试 git push 一个重置历史记录的分支,Git 就会抓住这个并抛出错误。Git 会假设被推送的分支不是最新的,因为它缺少提交。在这些情况下,git revert 应该是首选的撤销方法。

撤销上次提交

在上一节中,我们讨论了撤销提交的不同策略。这些策略也都适用于最近的提交。不过,在某些情况下,您可能不需要删除或重置上次提交。也许它只是过早制作的。在这种情况下,您可以修改最近的提交。在工作目录中进行了更多变更并使用 git add 将其暂存以供提交后,就可以执行 git commit --amend 了。这将让 Git 打开配置的系统编辑器,方便您修改最后的提交消息。新的变更将添加到修改后的提交中。

撤销未提交的变更

在将变更提交到存储库历史记录之前,它们存在于暂存索引和工作目录中。您可能需要撤销这两个区域内的变更。暂存索引和工作目录是内部 Git 状态管理机制。有关这两种机制如何运行的更多详细信息,请访问 git reset 页面,该页面对它们进行了深入探索。

工作目录

工作目录通常与本地文件系统同步。要撤销工作目录中的变更,您可以像往常一样使用自己喜欢的编辑器编辑文件。Git 有几个工具可以帮助管理工作目录。有 git clean 命令,这是一个用于撤销对工作目录的变更的便捷实用程序。此外,git reset 可以通过 --mixed--hard 选项调用,并将重置应用于工作目录。

暂存索引

git add 命令用于向暂存索引添加变更。Git reset 主要用于撤销暂存索引的变更。--mixed 重置会将所有待处理的变更从暂存索引移回工作目录中。

撤销公共变更

在使用远程存储库的团队中工作时,撤销更改时需要额外考虑。Git reset 通常应被视为“本地”撤销方法。撤销对私有分支的变更时应使用重置。这可以安全地隔离从其他分支中删除可能被其他开发人员使用的提交。当在共享分支上执行重置然后使用 git push 远程推送该分支时,就会出现问题。在这种情况下,Git 会阻止推送,表示被推送的分支在远程分支上已过时,因为它缺少提交。

撤消共享历史记录的首选方法是 git revert。还原比重置更安全,因为它不会从共享历史记录中移除任何提交。还原将保留要撤消的提交,并创建一个对不需要的提交做出反转的新提交。此方法对于共享远程协作而言更安全,因为远程开发人员可以拉取分支并接收对不需要的提交做出撤消的新还原提交。

摘要

我们介绍了许多在 Git 中撤销内容的高级策略。请务必记住,在 Git 项目中,“撤销”的方法不止一种。此页面上的大多数讨论都涉及更深层次的主题,这些主题在相关 Git 命令的特定页面上进行了更详尽的解释。最常用的“撤销”工具是 git checkout、git revertgit reset。需要记住的一些关键点是:

  • 一旦提交了更改,一般都会永久保留
  • 使用 git checkout 可移动和查看提交历史记录
  • git revert 是撤销共享公共变更的最佳工具
  • git reset 最适合用于撤销本地私有变更

除了主要的撤消命令,我们还查看了其他 Git 实用程序:git log 用于查找丢失的提交,git clean 用于撤消未提交的更改,git add 用于修改暂存索引。

每个命令都有自己的详细文档。要了解有关此处提到的特定命令的更多信息,请访问相应的链接。