Git 撤销提交和更改
在本节中,我们将讨论Git的“撤消”策略和命令。首先需要注意的是,Git 没有像文字处理应用程序那样的传统“撤销”系统。Git 有其自己的“撤销”操作,如 Git checkout, Git revert, Git clean,Git reset 等。
一个有趣的比喻是将 Git 视为时间机器,对时间线进行管理。一次commit提交是项目历史时间线中某个时间点的快照。此外,可以通过使用分支来管理多个时间线。在 Git 中进行“撤消”操作时,我们通常会回到过去,或者移到另一个没有发生错误的时间线。
对比差异:检索旧的提交
任何版本控制系统背后的思想都是存储项目的“安全”副本,这样我们就不必担心对代码库造成不可挽回地破坏。一旦我们对代码库建立了提交的项目历史记录,那么接下来就可以访问历史记录中的任何提交。
git log
命令是查看 Git 仓库历史提交记录的最佳的选择,也是后续其他操作的基础。如果你对该命令还不熟悉,推荐先去查看本教程的 git log章节。
在下面的示例中,我们使用git log来获取仓库 git-example 的历史提交信息。
$ git log --oneline
查询结果如下
每个提交都有一个唯一的 SHA-1 哈希标识符。这些 ID 用于遍历已提交的时间线并访问特定的提交的信息。默认情况下,git log将只显示当前所在分支的提交信息。然而有时候可能我们要查看的是其他分支的历史提交信息,这种情况就需要我们在 git log 命令后面加上 --branches=*
选项了。
要查看当前仓库有哪些分支和我们当前所在哪个分支,可以使用 git branch 命令,关于该命令,我们会在后面的章节中进行介绍。
从上图中可以看到,我们一共有两个分支 dev
和 master
,并且我们当前在dev分支上。
当找到要访问的提交记录点时,我们可以使用git checkout
命令来访问该提交。git checkout命令将这些保存的快照“加载”到我们的开发机器上。在正常的开发过程中,HEAD通常指向main 或其他一些本地分支,但是当我们使用 git checkout 检出之前的提交时,HEAD不再指向某个分支——它直接指向这个提交。这称为“分离HEAD”状态:
检出旧的文件不会对HEAD指针进行移动。它会保持在同一个分支和同一个提交上,避免了“分离 HEAD”状态。然后,我们就可以像提交任何其他更改一样在新快照中将就版本的文件提交。因此,实际上,git checkout对文件的这种用法可以作为恢复到单个文件的旧版本的一种方式。
假设我们已经开始开发一个新功能,但你不确定是否要保留它。为了做出决定,则需要在开始之前查看项目的状态。首先,我们需要找到要查看的修订的 ID。
$ git log --oneline
我们可以使用git checkout查看“新增撤销更改相关文件”这次提交,如下所示:
$ git checkout 26999f6
这使我们工作目录与 26999f6 提交的状态匹配。此时我们可以查看文件、编译项目、运行测试,甚至编辑文件,而不必担心丢失项目的当前状态。在此处所做的任何事情都不会保存在仓库中。要继续开发,需要回到项目的“当前”状态:
$ git checkout master
假设我们在默认 master 分支上进行开发。回到 master 分支后,可以使用 git revert
或 git reset
撤消任何不需要的更改。
撤消已提交的快照
从技术上讲,有几种不同的策略可以“撤消”提交。假设我们当前的提交历史如下
$ git log --oneline
05afb5b (HEAD -> master, dev) 增加撤销更改命令内容
26999f6 新增撤销更改相关文件
4430b62 添加网址
c8f042a Init Project
我们将撤消 “26999f6 新增撤销更改相关文件” 提交。可以有三种方式
使用 git checkout 撤销提交
使用git checkout
命令,我们可以检出要撤销的提交的之前的提交,4430b62。此时仓库置于 26999f6 提交发生之前的状态。检出特定的提交将使仓库处于“分离 HEAD ”状态。
我们可以看到,红色文字处表示当前的状态。这意味着我们不再在任何分支上工作。在分离状态下,当我们将分支更改回已建立的分支时,那么所做的任何新提交都将是孤立的。孤立的提交将由 Git 的垃圾收集器删除。垃圾收集器按配置的时间间隔运行并定期的永久销毁孤立提交。为了防止孤立提交被垃圾收集,我们需要确保我们在一个未分离的分支上。
从分离的HEAD 状态,我们可以执行git checkout -b new_branch_jiyik
命令。 这将创建一个名为 new_branch_jiyik 的新分支并切换到该状态。仓库现在位于新的历史时间线上,其中 26999f6 提交不再存在。
此时,我们可以继续在这个不再存在 26999f6 提交的新分支上工作,并认为它“已撤销”。不幸的是,如果我们需要前一个分支,也许它是我们的 master 分支,这种撤消策略是不合适的。让我们看看其他一些“撤销”方式。
使用 git revert 撤销提交
现在假设我们有如下的历史提交。
$ git log --oneline
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
如果我们想撤销 872fa7e 提交,可以执行以下 git revert 命令
$ 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提交。 与我们之前的策略不同,我们可以继续使用相同的分支。这是一个令人满意的撤销方式。如果需要保留最少的 Git 历史记录(将不需要的提交在历史记录中去除),则此策略可能也无法令人满意。
使用 git reset 撤消提交
git reset 是一个具有多种用途的命令。如果我们调用以下命令
$ git reset --hard a1e8fb5
提交历史将重置为指定的提交。现在的提交历史看起来如下所示
$ git log --oneline
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
上面输出中不再在提交历史记录中显示e2f9a78和872fa7e提交。此时,我们可以继续工作并创建新的提交,就好像 872fa7e 提交从未发生过一样。这种撤销更改的方法对历史有最清晰的影响。reset 对于本地仓库更改非常有帮助,但是在使用共享远程仓库时会增加复杂性。
如果我们有一个共享的远程仓库,并且已经将 872fa7e提交推送过去了。现在我们尝试 git push 我们重置过的提交历史记录的分支,Git 将捕获它并抛出错误。Git 会假设被推送的分支不是最新的,因为它缺少提交。在这种场景中,反过来就得需要使用git revert
撤销方式了。