Skip to content

git reset 详解

git reset 是 Git 中最强大也最容易误用的命令之一。它通过移动 HEAD(和当前分支指针)来"回退"历史,有三种不同的模式。

reset 的核心机制

git reset 做的事情是:移动 HEAD(和它指向的分支指针)到指定的提交

reset 前:
A ← B ← C ← D  (HEAD → main)

git reset B 后:
A ← B  (HEAD → main)
(C 和 D 不再被 main 引用,但对象仍存在于 .git/objects)

三种模式的区别在于:移动 HEAD 之后,如何处理暂存区工作区

--soft:只移动 HEAD

bash
git reset --soft HEAD~1
git reset --soft abc123

效果:

  • HEAD 移动到指定提交
  • 暂存区不变(保留被撤销提交的改动在暂存区中)
  • 工作区不变
reset --soft HEAD~1 前:
HEAD → D(已提交)
暂存区:空(与 D 一致)
工作区:无修改

reset --soft HEAD~1 后:
HEAD → C
暂存区:D 的改动(staged,等待重新提交)
工作区:无修改

使用场景:

  • 撤销提交,但保留改动在暂存区(准备重新提交)
  • 将多个提交合并成一个(先 reset --soft,再一次性 commit)
bash
# 场景:合并最近 3 个小提交为一个
git reset --soft HEAD~3
git commit -m "feat: 完整的用户认证功能"

--mixed(默认):移动 HEAD + 重置暂存区

bash
git reset HEAD~1
git reset --mixed HEAD~1   # 等价
git reset abc123

效果:

  • HEAD 移动到指定提交
  • 暂存区重置为指定提交的状态(被撤销的改动变为未暂存状态)
  • 工作区不变
reset --mixed HEAD~1 前:
HEAD → D(已提交)
暂存区:空(与 D 一致)
工作区:无修改

reset --mixed HEAD~1 后:
HEAD → C
暂存区:空(与 C 一致)
工作区:D 的改动(modified,未暂存)

使用场景:

  • 撤销提交和暂存,重新整理后再提交
  • git reset HEAD <file>:取消单个文件的暂存(不移动 HEAD)
bash
# 撤销最近提交,改动回到工作区(可以重新整理再提交)
git reset HEAD~1

# 取消某文件的暂存(不影响其他文件)
git reset HEAD src/app.js

--hard:移动 HEAD + 重置暂存区 + 重置工作区

bash
git reset --hard HEAD~1
git reset --hard abc123

效果:

  • HEAD 移动到指定提交
  • 暂存区重置为指定提交的状态
  • 工作区重置为指定提交的状态(工作区改动丢失!
reset --hard HEAD~1 前:
HEAD → D(已提交)
暂存区:部分改动
工作区:其他改动

reset --hard HEAD~1 后:
HEAD → C
暂存区:空(与 C 完全一致)
工作区:与 C 完全一致(所有未提交的改动都丢失!)

警告--hard永久丢失未提交的改动(工作区和暂存区中的内容)!执行前请确认。

使用场景:

  • 完全放弃某次提交及其之后的所有改动
  • 紧急回退到某个干净状态
  • 放弃所有本地未提交的改动(git reset --hard HEAD
bash
# 完全放弃最近提交(包括改动)
git reset --hard HEAD~1

# 放弃所有本地未提交的改动(等价于全量 restore)
git reset --hard HEAD

# 回退到某个标签时的状态
git reset --hard v1.0.0

三种模式的对比

             HEAD    暂存区    工作区
--soft       移动    不变      不变
--mixed(默认) 移动    重置      不变
--hard       移动    重置      重置(危险)
模式命令HEAD暂存区工作区改动去向
softreset --soft移动不变不变暂存区
mixedreset移动重置不变工作区
hardreset --hard移动重置重置丢失

reset 与 checkout 的区别

git resetgit checkout 都可以移动 HEAD,但有重要区别:

对比项git resetgit checkout
操作对象移动当前分支指针HEAD 指针(可能进入分离状态)
分支状态当前分支也移动了当前分支不变,HEAD 指向提交
影响改变分支历史不改变分支历史
用途回退历史检出历史状态
bash
# reset:main 分支也回退了
git switch main
git reset --hard HEAD~3
# main 现在指向 HEAD~3

# checkout:main 不变,进入分离 HEAD
git checkout HEAD~3
# main 仍在原位,HEAD 指向旧提交(分离状态)

针对文件的 reset

git reset 也可以只操作特定文件(此时不移动 HEAD):

bash
# 将 src/app.js 的暂存区状态重置为 HEAD 版本(取消暂存)
git reset HEAD src/app.js
git reset -- src/app.js    # 等价

# 将 src/app.js 的暂存区重置为 HEAD~2 版本
git reset HEAD~2 src/app.js

如何恢复 reset --hard 后的提交

虽然 --hard 会"丢失"提交,但 Git 并不会立即删除这些对象,可以通过 reflog 找回:

bash
# 查看 reflog 找到被 reset 前的 SHA-1
git reflog
# abc123 HEAD@{0}: reset: moving to HEAD~3
# def456 HEAD@{1}: commit: feat: 功能C(这是被丢弃的提交)
# ...

# 恢复到 reset 之前的状态
git reset --hard def456
# 或创建新分支保存
git branch recovered-work def456

常用 reset 场景

bash
# 撤销最近的错误提交(保留改动以便修改后重提)
git reset HEAD~1

# 彻底放弃最近的提交和所有本地改动
git reset --hard HEAD~1

# 合并最近 5 个小提交为一个
git reset --soft HEAD~5
git commit -m "feat: 完整的功能模块"

# 放弃所有本地未提交的改动(谨慎!)
git reset --hard HEAD

# 取消某文件的暂存
git reset HEAD config.json

总结

  • --soft:后悔提交,但改动是对的 → 保留在暂存区
  • --mixed(默认):后悔提交,需要重新整理 → 保留在工作区
  • --hard:完全后悔,放弃所有改动 → 彻底清除(危险!)

使用 --hard 前要三思,如果不确定,先用 reflog 作为后盾。