本节摘要:Tag 是钉在提交上的"发布图章",标记可发布版本与里程碑,一旦打好基本不动。本节先讲轻量标签与附注标签的本质区别,再覆盖打标、查看、推送、删除全套操作,最后讲签名标签、语义化版本、CI 触发三个进阶应用。核心结论:正式发布一律用附注标签,并且要显式推送到远程。
阅读完本节,你应当能够:
你发布了 v1.0,三个月后线上出了一个只有 v1.0 才有的 bug。你需要在代码库里找到"当时发布时的那一版代码"。如果你用的是提交哈希,麻烦来了:哈希是一串 40 位的乱码,你还记得住吗?就算记在发布文档里,团队其他人也未必知道去哪查。
Tag 解决的就是"给历史中某个提交起个永久、人类可读的名字"。它像一枚盖章:在一个时间点,这版代码就是 v1.0,这个坐标永远钉在那里,不随后续开发移动。
但 Tag 和分支有个关键区别必须讲清楚:分支会移动——你在 main 上每提交一次,main 指针就前进一步;Tag 不会移动——一旦钉上,它永远指向那个提交。这就带来一个常见误解:"我改了代码,为什么 v1.0 没变?" 因为 v1.0 那枚章盖的是历史,你改的是现在。Tag 的意义正是"盖过的章不能反悔"。
类比档案室:分支像"当前工作区"的标签——今天在这,明天挪到那;Tag 像档案袋上的"归档编号"——入档的那一版永远留在那儿,要回看、要取证、要复现,都认这个编号。仓库里可以有一百个分支同时活着,但只有少数几个编号值得盖章归档——那就是你的发布版本。
这是 Tag 最重要的一个选择,先把本质讲透:
轻量标签(lightweight):本质上只是一个指针,在 .git/refs/tags 目录下放一个文件,文件名是标签名,文件内容是被钉提交的哈希。它没有作者、没有日期、没有消息——就是一张写着"v1.0 → 某个哈希"的纸条。
附注标签(annotated):是 Git 数据库里的一个完整对象,包含打标签者的名字、邮箱、日期、一条标签消息,还能用 GPG 签名。它有自己的哈希,git show 能看到完整元数据。
| 维度 | 轻量标签 | 附注标签 |
|---|---|---|
| 存储形态 | 一个指向提交的引用文件 | 独立的 tag 对象 |
| 附带信息 | 无 | 作者、日期、消息、可签名 |
| 能否签名 | 不能 | 可用 GPG 签名 |
| 查看方式 | 只显示指向的提交 | git show 显示完整信息 |
| 适用场景 | 个人临时标记、实验 | 正式发布、团队里程碑 |
| 推荐度 | 少用 | 推荐,正式发布必用 |
为什么推荐附注标签?因为正式发布需要可追溯:v2.0 是谁在什么时候打的章、附了什么说明、是否被签名验证过——这些信息只有附注标签能提供。轻量标签连"谁打的"都查不到,只适合个人草稿级的标记。
理解标签的不可变性,要从它的存储位置看。轻量标签是 .git/refs/tags 下的一个引用文件,内容就是目标提交的哈希;附注标签虽然是个独立对象,但同样由 refs/tags 下的引用指向它。引用可以随时被改写——Git 并不禁止你把一个标签重新指向别的提交,git tag -f 就是干这个的。
那"标签不可变"指的是什么?指的是团队约定而非技术强制。一旦你把某个标签推送出去,它就成了"已发布的事实":别人可能已经基于 v1.0 构建了制品、签了交付单、或者在 CI 里以它为触发器。此时你悄悄把 v1.0 重新指向另一个提交,所有人的参照物就集体错乱了——版本号代表的内容不再唯一。所以行业共识是:已推送的标签永不移动,错了就删除重建,或直接发布一个更高的版本号。
这与分支形成鲜明对比。分支引用几乎每天都在动——main 每次合并都会前进——大家对此习以为常,因为分支代表"开发状态",天然是流动的。标签代表"发布事实",天然是固定的。一个仓库里,分支是无数条流动的线,标签是少数几个钉死的点,这个心理模型值得反复强化。
第一步:打标
git tag -a v1.0 -m "Release version 1.0" # 附注标签,推荐 git tag v1.0-lw # 轻量标签,临时用 git tag -a v0.9 1a2b3c4 -m "上一版补丁" # 给历史提交打标
-a 是 annotated,-m 是消息。不打 -m 会弹出编辑器让你写。不指定提交时默认打在 HEAD 上;指定哈希或分支名,可以钉在任何历史提交上。
第二步:查看
git tag # 列出全部标签 git tag -l "v1.*" # 按模式查找 git show v1.0 # 看附注标签的完整信息
第三步:推送
默认 git push 不推送标签——这是新手最容易踩的坑:本地打了章,远程仓库怎么没有?因为标签默认是本地动作,要显式推:
git push origin v1.0 # 推单个标签 git push origin --tags # 推所有本地标签
⚠️ 常见坑:git push origin --tags 会把所有本地标签一股脑推上去,包括你可能不想公开的实验性标签。团队协作里更稳妥的做法是逐个推要发布的标签,把推标签当成一次正式动作而不是批量操作。
第四步:删除
git tag -d v1.0 # 删本地标签 git push origin --delete v1.0 # 删远程标签
删除远程标签的旧写法是 git push origin :v1.0(冒号前缀表示推一个空引用过去),Git 1.8.5 之后的 --delete 更直观。注意:删掉标签不会删掉它指向的提交——历史还在,只是那枚"章"不见了。
标签指向提交,提交链是单向的;标签从本地到远程要靠显式推送,两件事不要混在一起。
理解标签在对象模型里的位置,能让你在"标签显示不出来""git show 和 git log 输出不一致"这类怪问题面前不慌。Git 的对象库里躺着四类对象:Blob(文件内容)、Tree(目录结构)、Commit(提交)、Tag(附注标签对象)。分支和轻量标签都不是对象,只是引用——引用是一个文本文件,写着目标对象的哈希。
所以 git show v1.0 时,Git 要顺着引用追两层:如果追到的是 Tag 对象,就显示标签信息加提交信息;如果追到的是 Commit,就只有提交信息。这解释了为什么同一命令对两种标签输出不一样——不是 Git 行为怪异,而是它们原本就指向不同的对象。凡是输出和预期不符,先问一句"我打的是哪种标签",八成问题出在这。
git checkout v1.0 会进入分离 HEAD 状态——你不在任何分支上,在此状态做的提交不属于任何分支,切回分支后这些提交很难再找到。想基于发布版本修 bug,正确姿势是先建分支:
git checkout -b hotfix/v1.0 v1.0
这样既拿到了 v1.0 的代码,又有一块安全的"工作台"。修完发布 v1.0.1,再打一个新标签。标签本身永远不动,这是它和分支的本质差异。
需要向外界证明"这个发布确实出自我们之手、未被篡改",用 GPG 签名:
git tag -s v2.0 -m "Signed release v2.0" git tag -v v2.0 # 验证签名
开源项目的发布流程几乎都带签名——下载源码的人能用你的公钥验证标签真实性。配置 GPG 需要先准备密钥环境,一次配好,之后打标就是加个 -s。
标签是语义化版本落地的基础设施。SemVer 的规则是主版本.次版本.修订号(如 1.2.3):不兼容的改动升主版本,加功能升次版本,修 bug 升修订号。每次发布,把对应的 SemVer 版本号打成附注标签:
v1.0.0 首个稳定版 v1.1.0 加了新功能 v1.1.1 修了 bug v2.0.0 不兼容的改动
标签名和版本号一一对应,git tag -l "v1.*" 一查就能列出该主版本线的全部发布。版本管理从此有了代码层面的坐标,而不是靠聊天记录和发布文档。
很多团队的发布流水线靠标签触发:开发推一个 v* 标签,CI 系统检测到后自动构建、测试、打包、部署。这让"发布"变成一个 git 动作而不是手动操作——打上标签,流水线就自动跑起来。也因此,标签名要规范、要稳定:CI 的匹配规则通常依赖命名约定(比如必须以 v 开头),打错名字就触发不了。
💡 关键直觉:把标签想成"出版盖章"。分支是"稿件改来改去",标签是"印成书的版本号"——书印出来就固定了,任何修订都是下一版(新标签),而不是改旧书。
"标签和分支到底什么区别?" 分支是移动的指针,跟着新提交走;标签是固定的指针,永远指向打标时的那个提交。分支用来开发,标签用来归档。
"打了标签还要推吗?" 要。标签默认只在本地,git push 不带它。想共享给团队或触发远程 CI,必须 git push origin 标签名 显式推送。
"标签名能重打吗?" 能,但有坑。同名标签再打会报错(除非 -f 强制覆盖)。更麻烦的是远程:同名标签在不同仓库指向不同提交时会造成混乱,团队约定里通常禁止移动已推送的标签。
"删标签会把代码删掉吗?" 不会。标签只是指向提交的引用,删除引用不影响提交对象本身。历史还在,只是少了那枚章。
"什么时候该用分支而不是标签?" 当你想"钉住当前状态但之后可能要继续开发"时用分支;当你想"永久归档一个状态、之后只回看不再改"时用标签。发布用标签,持续维护用分支。
"标签能指向标签吗?" 能。给标签打标签是允许的,但几乎没人这么用——它只会让引用关系绕弯。更常见的是用 git show 标签名 一层层追到提交。保持标签只指向提交,是最简单的用法。
"怎么规范团队标签命名?" 约定先行:版本标签统一带 v 前缀(v1.0.0)、环境标签带后缀(release-prod-2024-08-01)、临时标签标明用途。命名规范直接决定 CI 匹配规则好不好写——标签名就是流程的接口,接口要稳定。
下一节让 Git 在关键时刻"自己动起来"——Git Hook 钩子,把质量检查变成流程的一部分。