Git 附注标签:标签的对象标识,为什么与它指向的提交不同
发布脚本把标签解析出的标识和构建提交标识直接比较,发现不相等,于是认定打错标签。然而这个标签可能正是指向正确提交的附注标签。两边比较的对象类型不同:一边是装着发布说明的标签对象,另一边才是包含代码快照的提交对象。
轻量标签是一个引用,通常直接指向提交。附注标签除了引用,还创建单独的 tag 对象,保存目标、标注者、时间和说明,也可以包含签名。要验证构建来源,应先说明需要核对标签对象本身,还是需要核对它最终关联的提交。
让两种标签指向同一份代码
把代码保存为 demo.sh,用 bash demo.sh 运行。脚本建立临时仓库,创建一个提交,再给它加 light 和 release 两个标签。所有身份只在脚本环境中设置,创建的演示标签明确不签名,不访问远端,也不覆盖任何已有项目标签。
AI概念示意图:以抽象物件说明本文主题,不代表真实界面或运行结果。
#!/usr/bin/env bash
set -eu
work=$(mktemp -d)
export GIT_AUTHOR_NAME='Tag Demo'
export GIT_AUTHOR_EMAIL='tag@example.invalid'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
git init -q -b main "$work/repo"
cd "$work/repo"
printf 'alpha\n' > sample.txt
git add sample.txt
git -c commit.gpgsign=false commit -qm initial
git -c tag.gpgSign=false tag light HEAD
git tag --no-sign -a release -m 'Demo release' HEAD
commit=$(git rev-parse HEAD)
light=$(git rev-parse refs/tags/light)
tag_object=$(git rev-parse refs/tags/release)
peeled=$(git rev-parse --verify 'refs/tags/release^{commit}')
test "$light" = "$commit"
test "$tag_object" != "$commit"
test "$peeled" = "$commit"
test "$(git rev-parse 'refs/tags/release^{}')" = "$commit"
test "$(git cat-file -t refs/tags/light)" = commit
test "$(git cat-file -t refs/tags/release)" = tag
printf 'light-type=%s\n' "$(git cat-file -t refs/tags/light)"
printf 'release-type=%s\n' "$(git cat-file -t refs/tags/release)"
printf 'peeled-type=%s\n' "$(git cat-file -t "$peeled")"
blob=$(git rev-parse HEAD:sample.txt)
git -c tag.gpgSign=false tag file-only "$blob"
test "$(git cat-file -t refs/tags/file-only)" = blob
if git rev-parse --verify 'refs/tags/file-only^{commit}' \
> "$work/rejected.out" 2> "$work/rejected.err"; then
printf 'unexpected commit\n' >&2
exit 1
else
printf 'file-tag-commit-check=rejected\n'
fi
printf 'repo=%s\n' "$work/repo"先检查类型,再比较完整标识
前三行说明 light 解析为 commit,release 解析为 tag,而 release 剥离后又是 commit。断言同时核对轻量标签等于 HEAD、附注标签本身不等于 HEAD,以及剥离得到的提交等于 HEAD。验收的是对象关系,输出不依赖某次运行生成的具体哈希值。
这里使用完整的 refs/tags 路径,可以避免分支或其他引用同名带来的歧义。保留完整对象标识进行程序比较,展示时才考虑缩写。短标识长度和仓库对象数量有关,不宜把界面上的几位字符当作跨仓库、长期稳定的唯一依据。
空花括号与指定类型有什么不同
后缀 ^{} 会不断剥离标签,直到得到非标签对象;后缀 ^{commit} 则要求最后能解析为提交。发布脚本明确需要代码提交时,后者把类型要求也写进检查。两种写法在本文 release 上得到同一结果,却不能因此认为它们在所有标签上都等价。
代码随后给 sample.txt 对应的 blob 对象加一个 file-only 轻量标签。这个标签确实存在,cat-file 也能读取其类型,但要求解析为提交时必须失败。最后一行 file-tag-commit-check=rejected 验证了“引用可解析”和“满足构建输入要求”是两项检查。
如果标签指向另一个标签,递归剥离仍会继续处理。若直接打印第一个对象标识而不说明类型,日志读者很难判断哪一步已经完成。可以在构建记录中分别保存标签名称、标签对象标识和目标提交标识,让后续调查有清楚的对照。
附注信息与可信验证分开处理
附注标签带说明并不等于带签名;本例明确创建的是未签名附注标签。即使存在签名,也需要按团队采用的签名方式和可信身份规则验证,不能仅凭对象里出现一段签名文字就宣称来源可信。对象关系核对只回答这份标签关联哪份代码。
已发布标签的名称还可能在不同副本中解析为不同对象,因此复现构建时应记录当时解析出的标识。发现差异后先比较引用和目标,确认是哪一层变化,不要直接强制重打标签。更改共享标签会影响已经下载它的协作者,应有明确的发布流程。
这组实验不要求记住全部 Git 内部结构。遇到标签与提交哈希不同,先用类型检查确认手里的对象,再按实际需求剥离和验证。这样既能避免把正确的附注标签误判为错误,也能挡住虽然存在、却不能作为构建提交的输入。


