如何撤销和移除当前分支上的特定 Git Commit

GitBeginner
立即练习

引言

在本实验中,你将学习如何管理你的 Git commit 历史。你将练习使用 git reset 撤销更改,使用 git revert 安全地回滚已公开的 commit,以及通过交互式 rebase 从你的分支中移除特定的 commit。你还将学习如何使用 reflog 来恢复已被移除的 commit。这些都是维护清晰易懂的项目历史的关键技能。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 中级 级别的实验,完成率为 69%。获得了学习者 100% 的好评率。

检查 Commit 历史

在修改历史之前,你首先需要知道如何查看它。git log 命令是为此提供的主要工具。在本实验中,一个 Git 仓库已经被初始化,其中包含多个 commit 来模拟一个真实的项目历史。

让我们开始检查 commit 历史。我们将使用 --oneline 标志将每个 commit 显示在单行上,并使用 --graph 以图形方式显示 commit 历史,这对于可视化分支和合并非常有用。

在你的终端中执行以下命令:

git log --oneline --graph

你应该会看到为你创建的五个 commit 的列表,最新的 commit 在最上面。每一行显示一个唯一的 commit hash(其简短版本)和 commit message。

* 7d3d24a (HEAD -> master) chore: Add last-commit file
* 8a9f1b3 docs: Update documentation in file1
* c2e4d6f fix: Add a temporary file that should be removed
* 5e1a3b2 feat: Add file2.txt
* 1b4c0a9 feat: Add file1.txt

注意:你的 commit hash 将与上面的示例不同。这是正常的,因为每个 hash 都是唯一的。

花点时间回顾一下历史。你将在接下来的步骤中操作这些 commit。

使用 git reset 撤销上一个 Commit

有时你提交了一个 commit,但立刻意识到自己犯了错误。git reset 命令非常适合这种情况。它可以将当前分支的 HEAD 指针移动到之前的 commit,从而有效地“撤销”一个或多个 commit。

我们将使用 --soft 选项,它会撤销 commit,但会保留该 commit 中的更改在你的暂存区(index)。如果你想重新提交这些更改,例如使用不同的消息或与其他更改合并,这会很有用。

让我们撤销最近的 commit,“chore: Add last-commit file”。

git reset --soft HEAD~1

这里,HEAD~1 指的是当前 HEAD 前面的那个 commit。

现在,检查你的仓库状态:

git status

你会看到 last-commit.txt 现在列在“待提交的更改”(Changes to be committed)下。这意味着 commit 本身被撤销了,但文件更改仍然处于暂存状态。

On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   last-commit.txt

最后,再次查看日志以确认 commit 已从历史记录中移除:

git log --oneline --graph
* 8a9f1b3 (HEAD -> master) docs: Update documentation in file1
* c2e4d6f fix: Add a temporary file that should be removed
* 5e1a3b2 feat: Add file2.txt
* 1b4c0a9 feat: Add file1.txt

正如你所见,最后一个 commit 已从日志中移除,但你的工作安全地保存在暂存区中。

使用 git revert 恢复(撤销)一个 Commit

虽然 git reset 对于本地更改非常有用,但它会重写历史,如果你已经将 commit 与他人共享,这可能会带来问题。在公共分支上撤销更改的一种更安全的方法是使用 git revert。此命令会创建一个 的 commit,该 commit 应用了指定 commit 的逆向更改。

首先,让我们通过重新提交上一步的更改来清理工作目录。

git commit -m "chore: Add last-commit file again"

现在,假设我们想撤销添加 file2.txt 的那个 commit。这个 commit 在我们的历史记录中更靠前。我们将对其进行 revert。我们可以通过它相对于 HEAD 的位置来引用它。查看步骤 1 中的日志,“feat: Add file2.txt”是距离顶部第四个 commit,因此我们可以将其引用为 HEAD~3

运行 revert 命令:

git revert HEAD~3

这将打开你的默认文本编辑器(默认为 vim),其中包含一个预先填充的 commit message,例如“Revert 'feat: Add file2.txt'”。你只需保存并关闭文件即可接受默认消息。

vim 中,输入 :wq 并按 Enter 进行保存和退出。

现在,再次检查日志:

git log --oneline --graph

你将在顶部看到一个新的 commit,“Revert 'feat: Add file2.txt'”。

* 1e2d3f4 (HEAD -> master) Revert "feat: Add file2.txt"
* 7d3d24a chore: Add last-commit file again
* 8a9f1b3 docs: Update documentation in file1
* c2e4d6f fix: Add a temporary file that should be removed
* 5e1a3b2 feat: Add file2.txt
* 1b4c0a9 feat: Add file1.txt

原始 commit 仍然保留在历史记录中,但其更改已被新的 revert commit 撤销。你可以通过列出目录中的文件来验证这一点。

ls

你会注意到 file2.txt 已不再存在。

使用交互式 Rebase 移除 Commit

对于更复杂的历史记录操作,例如从分支中间移除一个 commit,你可以使用交互式 rebase (git rebase -i)。这是一个非常强大的命令,可以重写 commit 历史,因此应谨慎使用,尤其是在已与其他开发者共享的分支上。

我们的目标是移除带有消息“fix: Add a temporary file that should be removed”的 commit。查看当前日志,这个 commit 现在是 HEAD~3

开始交互式 rebase 过程:

git rebase -i HEAD~4

此命令将打开你的文本编辑器(默认为 vim),其中包含最近 4 个 commit 的列表。

pick fd5a181 fix: Add a temporary file that should be removed
pick 5f27a4d docs: Update documentation in file1
pick 284be6f chore: Add last-commit file again
pick 8a460c5 Revert "feat: Add file2.txt"

## Rebase 9403080..8a460c5 onto 9403080 (4 commands)
#
## Commands:
## p, pick <commit> = use commit
## d, drop <commit> = remove commit
## ...

要移除 commit,请将包含“fix: Add a temporary file that should be removed”的行中的 pick 单词更改为 drop(或 d)。

将此更改为:

pick fd5a181 fix: Add a temporary file that should be removed

更改为:

drop fd5a181 fix: Add a temporary file that should be removed

在 vim 中,按 i 进入插入模式,进行更改,然后按 Esc 退出插入模式。输入 :wq 并按 Enter 保存并退出。Git 将在新的历史记录之上重放剩余的 commit。

检查日志以查看结果:

git log --oneline --graph

“坏”的 commit 现在已从历史记录中消失。

* a5b4c3d (HEAD -> master) chore: Add last-commit file again
* f9e8d7c docs: Update documentation in file1
* 1e2d3f4 Revert "feat: Add file2.txt"
* 5e1a3b2 feat: Add file2.txt
* 1b4c0a9 feat: Add file1.txt

同时,检查目录中的文件。文件 bad-commit-file.txt 已被移除。

ls

使用 git reflog 恢复已移除的 Commit

如果你不小心移除了一个 commit 怎么办?Git 会在名为 reflog 的特殊日志中记录所有对 HEAD 指针的更改。这是你的安全网。你可以使用它来查找和恢复丢失的 commit。

让我们查看 reflog 来找到我们刚刚重写的历史。

git reflog

你将看到一个你执行过的操作列表。查找显示 rebase (finish): returning to refs/heads/master 的行。它下面的条目,可能是 HEAD@{1},代表 rebase 之前你的分支的状态。

a5b4c3d (HEAD -> master) HEAD@{0}: rebase (finish): returning to refs/heads/master
1e2d3f4 HEAD@{1}: rebase (start): checkout HEAD~3
...

我们可以使用 git reset --hard 将我们的分支恢复到这个之前的状态。这个命令具有破坏性,会丢弃任何未提交的更改,因此请谨慎使用。

让我们将分支重置到 rebase 之前的状态,在本例中是 HEAD@{1}

git reset --hard HEAD@{1}

你将看到一条消息,确认 HEAD 现在位于 rebase 开始之前的 commit。

HEAD is now at 1e2d3f4 Revert "feat: Add file2.txt"

现在,最后一次检查日志:

git log --oneline --graph

你应该看到 rebase 之前的历史记录:

* f461400 (HEAD -> master) Revert "feat: Add file2.txt"
* acea45c chore: Add last-commit file again
* c04b3f5 docs: Update documentation in file1
* 9403080 feat: Add file2.txt
* ee39412 feat: Add file1.txt

请注意,“fix: Add a temporary file that should be removed”这个 commit 仍然不存在——这是正确的!git reset --hard HEAD@{1} 命令将我们恢复到了交互式 rebase 开始之前的状态,而不是在我们移除 commit 之前。交互式 rebase 成功地从我们的历史记录中移除了那个不需要的 commit。你可以运行 ls 来确认 bad-commit-file.txt 仍然不存在。reflog 是从 Git 错误中恢复的宝贵工具。

总结

在本实验中,你学习了管理 Git commit 历史的实用技术。你首先使用 git log 检查历史记录。然后,你练习了使用 git reset --soft 撤销 commit,使用 git revert 安全地恢复更改,以及使用强大的 git rebase -i 命令移除特定的 commit。最后,你学会了如何使用 git reflog 作为安全网来恢复从历史记录中移除的 commit。这些技能对于维护一个干净且易于管理的项目历史至关重要。