☰
checkout 和 reset 到底改了哪里:HEAD、分支、index、worktree 的变化
2026/10/2 13:59:29 网站建设 项目流程

checkout 和 reset 到底改了哪里:HEAD、分支、index、worktree 的变化

前面已经讲过 Git 里最容易混淆的三份状态:

  • HEAD:当前提交,或者当前分支指向的提交。
  • index:下一次提交准备包含什么。
  • worktree:你磁盘上真正看到、正在改的文件。

再加上一个分支引用:

  • refs/heads/master、refs/heads/feature:分支名,本质是指向 commit 的指针。

checkout和reset难理解,是因为它们经常同时影响这几样东西。

如果只记一句话:

checkout 更像“换位置”;reset 更像“挪当前分支”。

但这句话还不够。真正要弄清楚,要问三个问题:

  • HEAD最后指向哪里?
  • index会不会变?
  • worktree会不会被覆盖?

checkout branch:换到另一个分支入口

假设当前状态是:

master -> commitA
feature -> commitB
HEAD -> master

执行:

mgit checkout feature

结果是:

HEAD -> refs/heads/feature
index -> commitB 的 tree
worktree -> commitB 的 tree

也就是说,checkout feature做了两件事。

第一,切换当前所在分支:

HEAD 从 master 换到 feature

第二,把暂存区和工作区恢复成 feature 指向的提交快照:

index/worktree 变成 commitB 的样子

所以你执行完 checkout 后,会看到文件内容也跟着变了。

这就是很多人第一次用 Git 时困惑的地方:我只是切换分支,为什么文件也变了?

因为分支不是一个目录副本。分支只是一个指针。checkout 做的是“让当前工作区呈现这个指针指向的快照”。

checkout commit:进入 detached HEAD

如果不是 checkout 分支,而是 checkout 一个 commit:

mgit checkout HASH

这时HEAD不再指向某个分支,而是直接指向某个提交:

HEAD -> HASH

这叫 detached HEAD。

人话就是:

你站在了某个历史提交上,但没有站在任何分支入口上。

在 detached HEAD 状态下继续提交,新的 commit 不是自动挂在 master 或 feature 下面的。真实 Git 会提示你小心这一点。

这不是 Git 把历史弄坏了,而是你暂时离开了“分支名”这个入口。

checkout 为什么要检查工作区

checkout 会改worktree。

只要一个命令会改工作区,就必须考虑一个风险:会不会覆盖用户还没提交的修改?

比如你当前有:

a.txt 在 worktree 里被手动改了
但还没有 add,也没有 commit

这时 checkout 到另一个分支,如果目标分支里的a.txt内容不同,就可能直接覆盖你的修改。

mini-git 的策略比较保守:

  • 如果 index 和当前 HEAD 不一致,说明有 staged changes,拒绝 checkout。
  • 如果 worktree 文件内容和 index 不一致,说明有 unstaged changes,拒绝 checkout。
  • 如果目标 tree 会覆盖未跟踪文件,也拒绝 checkout。

这比真实 Git 的某些情况更严格,但更适合学习。

因为它把规则讲清楚了:

会写 worktree 的命令,不能随便覆盖用户本地修改。

源码里cmd_checkout.c的主线也是这个思路:

解析目标 branch/commit
检查当前 index/worktree 是否安全
读取目标 commit 的 tree
恢复 index 和 worktree
更新 HEAD

其中安全检查就是 checkout 命令最重要的工程细节。

reset:移动当前分支

reset的核心动作不是“切换分支”,而是:

把当前分支指针移动到另一个 commit。

假设现在是:

HEAD -> master -> commit2
commit2 -> parent commit1

执行:

mgit reset --mixed commit1

结果不是 HEAD 换到另一个分支,而是 master 本身被挪了:

HEAD -> master -> commit1

这就是 reset 和 checkout 的一个关键区别:

  • checkout branch:HEAD 换到另一个分支入口。
  • reset commit:当前分支入口本身被移动。

所以 reset 的危险性更高一点。它会改变当前分支指向的提交。

不过注意:reset 不会删除 commit 对象。

commit2 只是暂时没有分支指向了,对象还在.git/objects里。真实 Git 可以通过 reflog 找回,mini-git 里也会记录 reset 的 reflog。

reset 的三种模式

reset 难点在于它有三种模式。

模式移动当前分支更新 index更新 worktree
--soft是否否
--mixed是是否
--hard是是是

一句话版:

  • soft:只撤提交,不动暂存区和工作区。
  • mixed:撤提交,也撤 add,但保留文件修改。
  • hard:撤提交、撤 add、覆盖工作区。

真实 Git 里reset默认是--mixed。mini-git 也保持了这个默认行为。

用两次提交理解 reset

假设你做了两次提交:

commit1:a.txt = one
commit2:a.txt = two

现在状态是:

HEAD -> master -> commit2

reset --soft commit1

执行:

mgit reset --soft commit1

结果:

master -> commit1
index 仍然像 commit2
worktree 仍然像 commit2

适用场景:

提交写错了,想撤掉 commit,但保留“已经 add 好”的状态,重新 commit。

reset --mixed commit1

执行:

mgit reset --mixed commit1

结果:

master -> commit1
index -> commit1
worktree 仍然像 commit2

适用场景:

提交不想要了,add 状态也不想要了,但文件修改还想保留。

这也是最常用的撤提交方式。

reset --hard commit1

执行:

mgit reset --hard commit1

结果:

master -> commit1
index -> commit1
worktree -> commit1

适用场景:

当前修改完全不要了,工作区也要回到 commit1。

这也是最危险的模式,因为它会覆盖工作区。

所以reset --hard之前要先确认:当前未提交修改真的不需要了。

checkout 和 reset 的核心区别

可以这样对比:

命令更像什么主要影响
checkout branch换到另一个分支入口HEAD、index、worktree
checkout commit站到某个历史提交HEAD、index、worktree
reset --soft挪当前分支branch/HEAD
reset --mixed挪当前分支并重置暂存区branch/HEAD、index
reset --hard挪当前分支并覆盖到目标快照branch/HEAD、index、worktree

如果还是混,可以记一个判断:

checkout 改“我现在站在哪里”;reset 改“当前分支指到哪里”。

源码里怎么落地

mini-git 里相关文件主要是:

  • src/commands/cmd_checkout.c
  • src/commands/cmd_reset.c
  • src/core/ref.c
  • src/core/tree.c
  • src/core/index.c

checkout 的关键逻辑是:

解析目标
如果是分支,准备让 HEAD 指向 refs/heads/xxx
如果是 commit,准备进入 detached HEAD
检查 index/worktree 是否干净
用目标 commit 的 tree 恢复工作区和 index
更新 HEAD

reset 的关键逻辑是:

解析目标 commit
根据模式决定是否读取目标 tree
soft 不动 index/worktree
mixed 用目标 tree 重建 index
hard 用目标 tree 重建 index 并恢复 worktree
最后移动当前分支引用

这里有一个细节:mini-git 的reset --mixed/--hard是先同步状态,成功后再移动引用。这样如果恢复 index/worktree 失败,不会出现“分支已经挪了,但文件没恢复成功”的半截状态。

面试里怎么说

可以这样答:

checkout 更偏切换当前位置。checkout 分支时,HEAD 会指向目标分支,同时 index 和 worktree 会恢复成目标分支对应 commit 的 tree;checkout 某个 commit 时会进入 detached HEAD。reset 更偏移动当前分支指针,soft/mixed/hard 的区别在于是否同步 index 和 working tree:soft 只动分支,mixed 动分支和 index,hard 三者都动。

如果追问为什么 checkout/reset 容易丢修改,可以补一句:

因为 checkout 和 reset --hard 都可能写 worktree。如果当前工作区有未提交修改,目标快照又要覆盖同名文件,就可能丢数据,所以实现上必须做安全检查或要求用户明确使用 hard。

总结

这一篇抓住五句话就够了:

  • checkout branch是换分支入口。
  • checkout commit是 detached HEAD。
  • checkout会恢复 index/worktree,所以要保护本地修改。
  • reset是移动当前分支指针。
  • soft/mixed/hard的区别,就是影响范围从 HEAD 扩大到 index,再扩大到 worktree。

理解这几个点,checkout、reset就不再是“玄学撤销命令”,而是几个明确状态之间的移动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询