在软件开发中,代码回滚是一项必备技能。无论是修复线上 bug,还是撤销不想要的功能,我们都可能需要将代码恢复到之前的某个版本。Git 提供了多种方式来回滚代码,但对于新手来说,git reset、git revert 和 git checkout 这几个命令的区别可能会让人感到困惑。
别担心,这篇文章会用最通俗易懂的语言,结合代码示例和实际案例,带你彻底搞懂 Git 的回滚操作。
三大回滚命令:reset、revert 和 checkout
首先,我们来认识一下 Git 中用于回滚的三个主要命令:
-
git reset: 直接移动HEAD指针到指定的提交,就像时光倒流一样,可以选择性地保留或丢弃之后的修改。 -
git revert: 创建一个新的提交来抵消某个历史提交的更改,它不会修改原有的提交历史。 -
git checkout: 切换到一个指定的提交版本,但会进入一个“分离头指针”(detached HEAD)的状态,主要用于查看历史版本。
接下来,我们将逐一解析这三个命令的用法和适用场景。
git reset:简单粗暴的“时光机”
git reset 是一个非常强大的命令,它可以将你的分支“重置”到指定的提交。这个命令有三种常用的模式:--soft、--mixed 和 --hard。
1. git reset --soft <commit_id>
- 作用:只移动
HEAD指针到指定的提交,不会修改暂存区(index)和工作区(working directory)的内容。 - 口语化解析:相当于告诉 Git:“嘿,假装我最新的提交是
<commit_id>,但是我刚刚做的那些修改都还在,并且已经添加到暂存区了,我随时可以重新提交。” - 适用场景:当你觉得上一次的提交信息写得不好,或者想将多个提交合并成一个时,
--soft是一个不错的选择。
案例:修改最后一次提交信息
假设你刚刚提交了一个版本,但发现提交信息有错别字。
# 查看提交历史,获取上一个版本的 commit_id
git log --oneline
# 假设最新的提交是 "feat: add new feature",但你想改成 "feat: add user login feature"
# 使用 --soft 回滚到上一个版本
git reset --soft HEAD~1
# 此时,上次提交的更改已经回到了暂存区
# 你可以重新提交并修改提交信息
git commit -m "feat: add user login feature"
2. git reset --mixed <commit_id>
- 作用:移动
HEAD指针,并且重置暂存区,但工作区的内容保持不变。 这是git reset的默认模式。 - 口语化解析:就像是说:“回到
<commit_id>这个版本吧,我之前添加到暂存区的东西都不要了,但是代码文件的修改还是留给我。” - 适用场景:当你发现暂存区里有一些不想提交的文件,或者想重新整理暂存区的内容时,可以使用
--mixed。
案例:从暂存区移除文件
你不小心 git add 了一些不需要提交的文件。
# 查看暂存区状态
git status
# On branch master
# Changes to be committed:
# (use "git restore --staged <file>..." to unstage)
# modified: config.js
# new file: temp.log
# 回滚到当前版本,清空暂存区
git reset --mixed HEAD
# 再次查看状态,会发现 config.js 和 temp.log 变成了未暂存的修改
git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
# temp.log
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git restore <file>..." to discard changes in working directory)
# modified: config.js
# 现在你可以只添加需要的文件并提交
git add config.js
git commit -m "update config"
3. git reset --hard <commit_id>
- 作用:移动
HEAD指针,同时重置暂存区和工作区。 - 口语化解析:这是最彻底的回滚,相当于说:“回到
<commit_id>,之后的所有提交、暂存区的修改和工作区的代码,我全都不要了!” - 适用场景:当你确定要完全放弃某个提交之后的所有更改时,可以使用
--hard。但请务必谨慎使用,因为它会永久删除你的本地修改。
案例:彻底放弃某个功能
你开发了一个新功能,提交了几个版本后发现这个功能完全是个错误,需要彻底废弃。
# 查看提交历史,找到开始开发这个功能之前的那个版本的 commit_id
git log --oneline
# 假设要回滚到 2a3b4c5 这个提交
git reset --hard 2a3b4c5
# 执行后,所有在 2a3b4c5 之后的提交和本地修改都会被删除
git reset 总结
| 模式 | HEAD 指针 |
暂存区 (Index) | 工作区 (Working Directory) |
|---|---|---|---|
--soft |
移动 | 不变 | 不变 |
--mixed |
移动 | 重置 | 不变 |
--hard |
移动 | 重置 | 重置 |
重要提示:如果你的提交已经推送到了远程仓库,强烈不建议使用 git reset 来修改历史。因为这会造成你本地和远程仓库的历史不一致,给团队协作带来麻烦。在这种情况下,git revert 是更安全的选择。
git revert:安全的“反向操作”
与 git reset 修改历史记录不同,git revert 会创建一个新的提交,这个新提交的内容是目标提交的“反向”操作。
- 口语化解析:这就像是你写错了一个字,不是用橡皮擦掉,而是在旁边重新写一个正确的字,并告诉大家“之前那个是错的”。
- 适用场景:当你需要撤销一个已经推送到远程仓库的提交时,
git revert是最佳选择。 它不会改变项目历史,因此对于协作开发非常安全。
案例:撤销一个已经发布的 bug
你发布了一个新版本,结果发现其中一个提交引入了一个严重的 bug。
# 查看提交历史,找到引入 bug 的那个提交的 commit_id
git log --oneline
# 假设引入 bug 的提交是 785dd5a
git revert 785dd5a
执行上述命令后,Git 会自动创建一个新的提交,这个提交会撤销 785dd5a 所做的所有更改。 Git 会弹出一个编辑器让你编辑这次 revert 操作的提交信息。保存并退出后,一个新的提交就生成了。
之后,你只需要将这个新的提交推送到远程仓库,就可以安全地修复线上的 bug。
git checkout:轻松的“版本穿越”
git checkout 命令通常用于切换分支,但它也可以用来切换到某一个历史提交。
- 作用:将工作区切换到指定提交的状态,此时
HEAD指针会指向这个提交,进入“分离头指针”(detached HEAD)状态。 - 口语化解析:这就像是坐上时光机回到了过去,你可以四处看看当时的代码是什么样的,但你在这个状态下做的任何修改和提交都不会属于任何一个分支。
- 适用场景:当你只是想查看一下旧版本的代码,或者基于某个旧版本创建一个新的分支时,可以使用
git checkout。
案例:查看旧版本的代码
你想看一下上周某个功能的具体实现。
# 查看提交历史,找到你想查看的版本的 commit_id
git log --oneline
# 切换到指定的提交
git checkout a1b2c3d
现在你的工作目录就变成了 a1b2c3d 这个提交时的样子。你可以随意浏览和测试代码。
如果你想在这个版本的基础上进行新的开发,可以创建一个新的分支:
git checkout -b new-feature-branch
当你查看完毕,想回到最新的版本时,只需要切换回原来的分支即可:
git checkout master
总结:我该用哪个?
| 命令 | 主要作用 | 是否修改历史 | 对远程仓库的影响 | 适用场景 |
|---|---|---|---|---|
git reset |
移动 HEAD 和分支指针,可以选择性地重置暂存区和工作区 |
是 | 危险,不推荐用于已推送的提交 | 本地分支的回滚,整理提交历史 |
git revert |
创建一个新的提交来撤销历史提交的更改 | 否 | 安全 | 撤销已经推送到远程仓库的提交 |
git checkout |
切换到指定的提交,进入“分离头指针”状态 | 否 | 无 | 查看历史版本的代码,基于旧版本创建新分支 |
希望通过这篇文章,你能对 Git 的回滚操作有一个更清晰的认识。记住,多在本地创建实验分支进行练习,才能在实际工作中游刃有余!