bigywater.tech
Git · 54

reset 的 soft、mixed、hard 对三个区域的影响

--soft、--mixed、--hard 到底各改变了哪几个区域?哪些改动会真的丢失?--keep 和 --merge 又保护什么?这篇用同一个起点(HEAD、暂存区、工作区各不相同)逐个模式对账。

1 / 2 / 3
--soft 动 1 个区域(HEAD),--mixed 动 2 个(+ 暂存区),--hard 动 3 个(+ 工作区)
没 add 过
--hard 真正丢掉的只有从没 git add 过的工作区改动;已提交的有 reflog,add 过的有 blob
-- 路径
带路径的 reset 只改暂存区,HEAD 和工作区都不动,等价于“取消暂存”

三个区域

实测 · 本机执行输出
  HEAD=c3  | 暂存区 x='版本 3 + 暂存的改动'               | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在

git 里一份文件有三个版本:HEAD(最近提交)、暂存区、工作区。reset 的三种模式决定向这三个区域里的几个同步到目标提交。

三种模式

实测 · 本机执行输出
  git reset --soft: HEAD=c2  | 暂存区 x='版本 3 + 暂存的改动'               | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在
  git reset --mixed: HEAD=c2  | 暂存区 x='版本 2'                       | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在
  git reset --hard: HEAD=c2  | 暂存区 x='版本 2'                       | 工作区 x='版本 2'                         | 未跟踪 z 还在
  规律:--soft 只动 HEAD;--mixed(默认)动 HEAD 和暂存区;--hard 三个都动(未跟踪文件不碰)

--soft 只移动 HEAD(及当前分支);--mixed(默认)再让暂存区与目标一致,工作区不动;--hard 连工作区也改成目标的内容。未跟踪文件三种模式都不碰。

会丢什么

实测 · 本机执行输出
  c3 还在对象库里: True;暂存过的 blob 内容: '版本 3 + 暂存的改动'(从没 add 过的“工作区的改动”则没有对象,真的丢了)
  $ git reflog -3 --format='%gd: %gs'
  HEAD@{0}: reset: moving to HEAD~1
  HEAD@{1}: commit: c3
  HEAD@{2}: commit: c2
  ORIG_HEAD 指向被 reset 掉的 c3: True

--hard 覆盖掉的工作区改动里,只有从来没 git add 过的部分是真的丢了;add 过的内容作为 blob 仍在对象库里,被 reset 掉的提交可以通过 reflog 和 ORIG_HEAD 找回。

--keep 与 --merge

实测 · 本机执行输出
  git reset --keep HEAD~1 → error: Entry 'x' would be overwritten by merge. Cannot merge. / fatal: Could not reset index file to revision 'HEAD~1'.   | HEAD=c3  | 暂存区 x='版本 3 + 暂存的改动'               | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在
  git reset --merge HEAD~1 → error: Entry 'x' not uptodate. Cannot merge. / fatal: Could not reset index file to revision 'HEAD~1'.   | HEAD=c3  | 暂存区 x='版本 3 + 暂存的改动'               | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在
  对比:本地改的是 y(c1 到 c2 没碰 y)时 --keep 可以成功: HEAD=c1,x='1',y='y 的本地改动'(本地改动保留)

两者在会覆盖有本地改动的文件时拒绝执行,不像 --hard 无条件覆盖;只改了目标无关的文件时则正常成功并保留本地改动。

带路径的 reset

实测 · 本机执行输出
  $ git reset HEAD~1 -- x; git status --short
  Unstaged changes after reset:
  M	x
  MM x
  ?? z
  HEAD=c3  | 暂存区 x='版本 2'                       | 工作区 x='版本 3 + 暂存的改动 + 工作区的改动'        | 未跟踪 z 还在
  HEAD 没动: True
  $ git reset -- x; git status --short
  Unstaged changes after reset:
  M	x
   M x
  ?? z

git reset <提交> -- 路径 只替换暂存区里这些路径的内容,HEAD 和工作区都不动;不写提交就是用 HEAD,即“取消暂存”。

只动当前分支

实测 · 本机执行输出
  $ git log --oneline --all --decorate | sed -E 's/^[0-9a-f]{7} //'
  (keep) c3
  c2
  (HEAD -> main) c1

reset 只是移动当前分支的指针,别的分支不受影响。

分步演示

reset 的三种模式
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。每个模式在单独的仓库里重做;状态摘要来自 git log、git show :x、cat;提交时间固定
    • 没有验证:reset 到不是当前历史祖先的提交;reset 的 --recurse-submodules;reset 与 sparse-checkout、skip-worktree 的交互(见相关两篇);--patch(reset -p);rebase 过程中的 reset;git restore / git switch 与 reset 的对应关系(restore --staged 等价于 reset -- 路径,没有实测)
    京ICP备2026067502号-1京公网安备11010502063487号