| English | 交互演示 | 可运行实验 |
当 Agent 说“我已经切到另一个分支”或“我给这个版本打了标签”,真正需要确认的是:哪个引用现在指向哪个 commit,HEAD 是附着在分支上,还是直接指向一个 commit。
这决定了下一次 commit 会移动哪条引用,也决定了某个提交会不会因为离开 detached HEAD 而难以再次找到。
HEAD -> refs/heads/main -> C3
refs/heads/agent/draft -> C2
refs/tags/v1-light -> C1
refs/tags/v1-annotated -> T1(tag object) -> C1
refs/heads/ 的可移动引用,通常指向一个 commit。refs/tags/ 的引用。轻量标签直接指向对象;附注标签先指向 tag object,再由该对象指向目标对象。ref: refs/heads/main 的符号引用。提交时,Git 创建一个新 commit,并前移 main。gitrevisions 把 branch 名、tag 名和完整对象 ID 都作为 revision 的写法;HEAD 表示当前工作树所基于的 commit。官方 revision 文档
临时仓库中有三个 commit:
C1 baseline
├── C2 agent draft refs/heads/agent/draft
└── C3 main release HEAD -> refs/heads/main
v1-light -> C1
v1-annotated -> tag object -> C1
C1、C2、C3 都是对象。main、agent/draft、v1-light 和 v1-annotated 是给对象取名的引用。一个对象可以被多个引用命名;移动其中一个名字不会让其他名字或旧对象变成另一个内容。
这解释了分支切换的关键区别:切换分支会让 HEAD 改为附着到另一个 branch;创建分支只是新增一个指向现有 commit 的名字;在某分支创建 commit 时,只有该分支的引用会前移。
| 名称 | 常见位置 | 指向什么 | 创建新 commit 时会怎样 |
|---|---|---|---|
| branch | refs/heads/ |
通常是 commit | 当前附着分支前移 |
| lightweight tag | refs/tags/ |
直接是目标对象,常见为 commit | 保持原值 |
| annotated tag | refs/tags/ |
tag object,该对象再指向目标 | 保持原值 |
| attached HEAD | .git/HEAD |
某条 branch 的符号引用 | 跟随该 branch |
| detached HEAD | .git/HEAD |
直接是 commit ID | HEAD 前移,branch 不动 |
git symbolic-ref HEAD 可以读取 HEAD 所指向的 branch。官方文档说明,符号引用是以 ref: refs/ 开头的普通文件;当 HEAD 已 detached,带 -q 的读取命令会非零退出。git-symbolic-ref
在附着状态:
HEAD -> main -> C3
git commit
HEAD -> main -> C4
在 detached 状态:
HEAD -> C1
git commit
HEAD -> C4
main -> C3
agent/draft -> C2
两种状态都可以创建 commit。风险在于 detached commit 没有 branch 名自动承接。继续切换或做其他引用更新后,它可能只剩下 HEAD reflog 的短期线索。
需要保留 detached 工作时,在离开前创建一个 branch:
git branch agent/experiment HEAD
git switch main
这两个命令不复制项目文件,也不复制 commit 对象。它们新增一个引用,再切回主线。
Git 官方文档规定:未使用 -a、-s 或 -u 时,git tag 创建直接指向目标对象的 lightweight tag;使用这些选项会创建 tag object。附注标签包含 tagger、日期、消息和可选签名,轻量标签只是对象的名称。git-tag
用对象类型检查区别:
git cat-file -t v1-light
git cat-file -t v1-annotated
git rev-parse 'v1-annotated^{}'
预期:
commit
tag
<baseline-commit-id>
最后一条中的 ^{} 会解开 tag object,得到它最终指向的非 tag 对象。发布版本通常需要附注标签,因为它带有可审查的元数据;临时定位或本地试验常用 lightweight tag。团队还需单独约定标签是否可以移动、是否必须签名和如何发布。
实验数据和本章图一致:service.txt 的 baseline 是 release=base,agent/draft 新增 agent.txt,main 把 service.txt 更新为 release=main-1。
从仓库根目录运行:
lab_path=$(bash labs/git-mental-model/04-refs/setup.sh)
bash labs/git-mental-model/04-refs/verify.sh "$lab_path"
预期输出中的对象 ID 会因 Git 版本和对象格式不同而变化,结构固定:
ok: HEAD -> refs/heads/main -> <main-id>
ok: refs/heads/agent/draft -> <agent-id>
ok: refs/tags/v1-light -> <baseline-id>
ok: refs/tags/v1-annotated -> tag -> <baseline-id>
setup.sh 只输出临时目录路径,并禁用系统和全局 Git 配置、提交签名、标签签名和 hooks 对本实验的干扰。
git -C "$lab_path" for-each-ref \
--format='%(refname:short) %(objecttype) %(objectname)' \
refs/heads refs/tags
git -C "$lab_path" symbolic-ref --short HEAD
git -C "$lab_path" rev-parse HEAD
git -C "$lab_path" rev-parse refs/heads/main
预期语义:
agent/draft points at a commit
main points at a different commit
v1-light has object type commit
v1-annotated has object type tag
HEAD is attached to main
HEAD and refs/heads/main resolve to the same commit ID
比较 main 与 agent/draft 的 ID,可以看到两条 branch 同时存在。查看它们指向的文件:
git -C "$lab_path" show refs/heads/main:service.txt
git -C "$lab_path" show refs/heads/agent/draft:service.txt
git -C "$lab_path" show refs/heads/agent/draft:agent.txt
预期先看到 release=main-1,再看到 release=base 和 proposal=retry。
以下操作只适用于刚创建的实验目录。它会创建一个新的临时 commit:
git -C "$lab_path" switch --detach v1-annotated
git -C "$lab_path" symbolic-ref -q HEAD || printf 'HEAD is detached\n'
printf 'keep this detached commit\n' > "$lab_path/detached.txt"
git -C "$lab_path" add detached.txt
git -C "$lab_path" commit -m "detached experiment"
git -C "$lab_path" branch keep-detached HEAD
git -C "$lab_path" switch main
预期关键输出:
HEAD is detached
随后 keep-detached 指向刚创建的 commit,main 仍指向步骤 1 的 main commit。删除 keep-detached 前,应先判断该 commit 是否还需要保留或分享。
bash labs/git-mental-model/04-refs/cleanup.sh "$lab_path"
清理脚本要求目录内存在本实验专用标记,拒绝删除仓库根目录、用户主目录和根目录。
重写本地历史、移动分支或强制移动标签前,先保存当前引用:
git status --short
git branch safety/before-rewrite HEAD
git show --no-patch --oneline safety/before-rewrite
git reflog show -n 5 HEAD
这会新增一个本地 branch,给当前 commit 一个明确名字。它不处理未提交编辑,也不代替远端备份。已经推送且有他人基于其开发的历史,需要先确认协作影响;多数共享撤销场景应评估 git revert。
git tag -f 会移动既有标签名。若该标签代表已发布版本、构建输入或签名声明,移动它会改变下游解析到的对象。先建立新的标签命名或获得团队确认。
若实验、bisect 或查看旧版本时要提交,先运行:
git branch agent/experiment HEAD
这个 branch 是恢复线索。它仍在本地,尚未推送就不构成远端备份。
分支是一个引用。切换分支会让 HEAD 和工作树对齐到另一个 commit 的快照,branch 本身不保存一套独立文件副本。
切换和前移 branch 改变引用目标。新内容由新 commit 表达,旧 commit 仍保留原有对象 ID 和内容。
Git 可以强制更新 tag 引用。发布流程通常把已发布标签视为稳定标识,是否允许移动属于团队发布约定。
HEAD 在附着状态通过符号引用间接指向 branch;detached 时直接记录 commit ID。先检查 HEAD 的解析方式,才能判断下一次 commit 移动哪条引用。
它可以提交。问题在于新 commit 没有 branch 自动承接,离开前应创建 branch。
Agent 为了验证旧版本运行了:
git switch --detach <old-commit>
git commit -am "try a compatibility fix"
它随后报告“修改已提交”。验收时应回答两个问题:
答案:此时 HEAD 直接指向新 commit,没有常规 branch 前移。先用 git log -1 --decorate HEAD、git symbolic-ref -q HEAD 和 git show-ref --heads 检查状态,再执行 git branch agent/compatibility-test HEAD。最后检查新 branch 指向的对象 ID,并明确该 branch 是否需要推送或建立其他备份。