项目更新到最新版后异常报错,需要回到上一个版本 —— 这是最典型的回滚场景。原笔记只记了 reset --hard + push -f 这一条路,这次把几种方案补全,并说明什么时候不能用它。

一、先搞清三个区域

Git 的操作都绕不开这三个地方,理解它们,reset 就不再靠死记:

1
2
3
工作区(你编辑的文件)  --git add-->  暂存区(Index)  --git commit-->  版本库(HEAD)
↑ ↑ ↑
改了还没 add add 了还没 commit 已经提交

二、找到要回滚到哪一次提交

1
2
3
4
git log                    # 查看提交历史(从新到旧)
git log --oneline # 精简版,一行一条 ⭐ 最常用
git log --oneline -10 # 只看最近 10 条
git reflog # ⭐ 查看所有操作记录(含被"删掉"的提交)

输出长这样:

1
2
3
4
$ git log --oneline
4ad24e8 (HEAD -> main) 修复登录报错
8f3c1a2 添加用户模块
d91b077 初始化项目

4ad24e8 就是 commit hash(版本号),回滚时用它指定目标位置。

git reflog 是后悔药。它记录了 HEAD 的每一次移动,包括你 reset --hard 掉的提交。只要操作过(commit 过),几乎都能用 reflog 找回来 —— 见第七节。

三、reset 的三种模式(关键区别)

git reset 把 HEAD 移到指定提交,但工作区和暂存区怎么处理,由参数决定:

参数 版本库 HEAD 暂存区 工作区(你的文件) 危险性
--soft 回退 保留 保留 低:改动都还在,只是撤了 commit
--mixed(默认) 回退 重置 保留 中:撤了 commit 和 add
--hard 回退 重置 删除 ⚠️ 高:改动直接丢失
1
2
3
git reset --soft  8f3c1a2    # 只是撤销提交,改动保留在暂存区,可重新 commit
git reset --mixed 8f3c1a2 # 撤销提交和 add,改动回到工作区
git reset --hard 8f3c1a2 # 彻底回到 8f3c1a2 的状态,之后的改动全丢弃

--hard 会永久丢弃目标提交之后的所有改动(未 commit 的内容也会一起没)。执行前先确认没有需要保留的代码 —— 不确定就先 git stash 或新建分支备份。

四、场景一:回滚本地提交(还没 push)

最安全的情况,因为只影响你自己的仓库。

1
2
3
4
5
6
7
8
9
# 1. 查看历史,找到目标版本
git log --oneline

# 2. 回退(想保留改动用 --soft,不要了用 --hard)
git reset --hard 8f3c1a2

# 3. 确认结果
git log --oneline
git status

此时本地已经回到上一个版本。

如果 git reset --hard 之后发现回退错了,git log 里已经看不到那些提交了 —— 别慌,用 git reflog 找到原来的 hash,再 git reset --hard <那个hash> 就能回来。

五、场景二:回滚已推送到远程(GitHub)

git reset 只改本地,GitHub 上的代码不会跟着变。想让远程也回退,必须强制推送 —— 但这一步是团队协作中最危险的操作之一。

方案 A:reset + 强制推送(原笔记的做法)

1
2
git reset --hard 8f3c1a2     # 本地回退
git push -f # 强制推送,覆盖远程

仅在满足全部以下条件时使用:

  • ✅ 这是你个人的仓库 / 分支
  • ✅ 确定没有别人基于这些提交继续开发
  • ✅ 已通知可能受影响的人

-f(--force)会用你的本地历史直接覆盖远程,别人的提交会被抹掉。多人协作的主分支上这么做,同事下次 pull 会丢失工作。

更安全的写法是 git push --force-with-lease:推送前先检查远程是否有你不知道的新提交,有则拒绝推送,避免误覆盖。

方案 B:revert(推荐,不改写历史)

revert 的思路不是「删掉历史」,而是新建一次提交,把之前的改动反向抵消。历史完整保留,所有人正常 pull 即可同步。

1
2
3
4
5
6
7
8
9
10
11
12
# 撤销某一次提交(会打开编辑器填写提交信息)
git revert 4ad24e8

# 直接提交,不打开编辑器
git revert --no-edit 4ad24e8

# 撤销连续多次提交(不包含起点 d91b077,包含终点 4ad24e8)
git revert --no-commit d91b077..4ad24e8
git commit -m "回滚 3 月 8 日的功能改动"

# 推送,普通 push 即可,不需要 -f
git push

revert 是团队协作下的标准做法。它不改写历史,因此可以安全地在 main / master 上使用,也不需要强制推送。

两个方案怎么选

对比项 reset + push -f revert + push
是否改写历史 ✅ 删掉提交记录 ❌ 新增一次反向提交
需要强制推送 ✅ 需要 ❌ 不需要
影响他人 可能丢失他人提交 无影响
能否撤销回滚 需靠 reflog 再 revert 一次即可
适用场景 个人仓库、刚 push 的错误提交 团队协作、公共分支

六、撤销 merge 提交

revert 一个 merge 提交时需要指定保留哪条主线:

1
git revert -m 1 <merge-commit-hash>

-m 1 表示保留第一个父提交(通常是合入时所在的分支,如 main)。用错会回滚到错误的分支状态,执行前用 git log --graph --oneline 看清分叉结构。

七、误操作恢复:reflog 救命

1
2
3
$ git reflog
8f3c1a2 HEAD@{0}: reset: moving to 8f3c1a2
4ad24e8 HEAD@{1}: commit: 修复登录报错 ← 被 reset 掉的提交在这里
1
git reset --hard 4ad24e8      # 回到误操作前的状态

reflog 默认保留 90 天(未被引用的提交 30 天后可能被 GC 清理)。所以要恢复就尽快,别拖。

八、其它常见撤销场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 只撤销某一个文件到指定版本(不影响其它文件)
git checkout 8f3c1a2 -- src/main.js
# 新版 Git 推荐写法
git restore --source=8f3c1a2 -- src/main.js

# 撤销工作区未 add 的修改(危险,改动会丢失)
git checkout -- src/main.js
git restore src/main.js

# 撤销已 add 但未 commit(把文件从暂存区移回工作区)
git restore --staged src/main.js

# 修改上一次的提交信息(还没 push 时)
git commit --amend -m "新的提交信息"

# 临时保管改动,之后再取回
git stash # 存起来
git stash pop # 取回来

九、删除中间某次提交(交互式 rebase)

想「去掉」历史上某一次提交(而不是反向抵消),用交互式 rebase:

1
git rebase -i 8f3c1a2        # 对 8f3c1a2 之后的所有提交做整理

编辑器里把目标行前的 pick 改成:

指令 作用
pick 保留这次提交
drop(或 d) 删除这次提交
squash(或 s) 合并到上一次提交
reword(或 r) 只改提交信息
edit(或 e) 停下来修改这次提交

rebase 同样改写历史,已推送的分支用完后必须 push -f,风险和第五节方案 A 一样。公共分支请改用 revert。

十、GitHub 网页端回滚

不想敲命令的话,GitHub 也提供了图形化入口:

  1. 进入仓库 → 点击 Commits 查看提交历史
  2. 点开要撤销的那次提交,右上角点 Revert
  3. GitHub 会自动创建一个 revert 的 PR,确认合并即可

这本质就是 git revert,安全且可追溯,适合不熟悉命令行的场景。

十一、安全准则

  1. 回滚前先备份:git branch backup-20240308 建个分支,随时能回来
  2. 公共分支只用 revert,不用 reset + push -f
  3. 强制推送一律用 --force-with-lease 代替 -f
  4. 用 --hard 前先 git stash 或确认工作区干净
  5. 重要分支在 GitHub 上开启 branch protection,禁止强制推送

一句话记忆:自己的分支、刚提交的错 → reset --hard;别人也在用的分支 → revert。拿不准就用 revert,它永远安全。