Git 标签快照管理

跨多个 FPGA 仓库创建、管理和清理 Git 标签的标准实践。本文档定义了统一的标签命名规范,并提供了批量操作的自动化脚本。


1. 统一标签格式

1.1 格式规范


snapshot-YYYYMMDDHHmmss

字段 含义 示例
snapshot- 固定前缀,标识快照标签 snapshot-
YYYY 4 位年份 2026
MM 2 位月份(01-12) 07
DD 2 位日期(01-31) 24
HH 2 位小时(00-23) 15
mm 2 位分钟(00-59) 30
ss 2 位秒(00-59) 45
完整示例: snapshot-20260724153045

1.2 设计理由

1.3 实际效果展示


$ git tag -l "snapshot-*" | tail -5
snapshot-20260720140022
snapshot-20260721101503
snapshot-20260722184530
snapshot-20260723113015
snapshot-20260724153045


2. 附注标签 vs 轻量标签

2.1 对比

特性 轻量标签 (git tag <name>) 附注标签 (git tag -a <name>)
存储方式 指向提交的指针(ref) 完整的 Git 对象,拥有独立 SHA
元数据 标签者姓名、邮箱、日期、消息
git describe 仅返回最近的附注标签 正常使用
git show <tag> 显示提交内容 显示标签消息 + 提交内容
推送到远程 git push --tags 或显式推送 git push --tags 或显式推送
GPG 签名 不支持 git tag -s 签名标签
适用场景 临时本地标记 永久性快照

2.2 我们的策略:始终使用附注标签

所有快照均使用附注标签。 标签消息记录了自上次快照以来的变更内容,这对于二分查找和回滚决策非常宝贵。

# 正确(附注标签)
git tag -a snapshot-20260724153045 -m "FPGA Top: added ILA debug hub, 8 probe ports"

# 错误(轻量标签 — 不要使用)
git tag snapshot-20260724153045

2.3 标签消息约定

消息应简洁但信息充分。包含受影响的模块或文件区域:


<Area>: <short description of changes since last tag>

示例:
- "fcam: fixed Bayer-to-RGB pipeline stall on frame boundary"
- "top: integrated soft_ila_top v2.3, added 4 trigger ports"
- "axi_interconnect: increased outstanding transaction limit to 16"
- "All repos: pre-release baseline for v1.4.0-rc1"

2.4 查看标签详情


# 显示标签元数据 + 指向的提交
git show snapshot-20260724153045

# 列出所有标签及其消息(每行一个)
git tag -l "snapshot-*" --format='%(refname:short) %(taggerdate:short) %(subject)'

# 显示两个快照之间的提交
git log --oneline snapshot-20260723113015..snapshot-20260724153045

# 显示两个快照之间变更的文件
git diff --stat snapshot-20260723113015..snapshot-20260724153045


3. 5 个 FPGA 仓库批量打标签脚本

3.1 5 个标准仓库


# 默认 FPGA 仓库列表
FPGA_REPOS=(
    "[email protected]:BuckHuang/fpga_top.git"
    "[email protected]:BuckHuang/fcam.git"
    "[email protected]:BuckHuang/axi_interconnect.git"
    "[email protected]:BuckHuang/video_pipeline.git"
    "[email protected]:BuckHuang/ip_common.git"
)

3.2 批量打标签脚本:tag-all.sh


#!/bin/bash
# /home/user/tools/tag-all.sh
# 在所有 5 个 FPGA 仓库中创建附注快照标签。
#
# 用法:
#   tag-all.sh                          # 使用自动生成的时间戳打标签
#   tag-all.sh "fixed CDC bug"          # 使用自定义消息打标签
#   tag-all.sh -d                       # 干运行(仅打印将执行的操作)
#   tag-all.sh -r                       # 同时将标签推送到远程

set -euo pipefail

# --- 配置 ---
REPOS_BASE="/home/user/work"
REPOS=(
"fpga_top"
"fcam"
"axi_interconnect"
"video_pipeline"
"ip_common"
)
REMOTE_PUSH=false
DRY_RUN=false
TAG_MESSAGE=""

# --- 解析参数 ---
while getopts "dr" opt; do
case $opt in
d) DRY_RUN=true ;;
r) REMOTE_PUSH=true ;;
*) echo "Usage: $0 [-d] [-r] [tag message]" ; exit 1 ;;
esac
done
shift $((OPTIND - 1))

TAG_MESSAGE="${*:-routine snapshot}"

# --- 标签名称 ---
TIMESTAMP=$(date +%Y%m%d%H%M%S)
TAG_NAME="snapshot-${TIMESTAMP}"

echo "=== Snapshot Tag: ${TAG_NAME} ==="
echo "=== Message: ${TAG_MESSAGE} ==="
echo ""

SUCCESS=()
FAILED=()

for repo in "${REPOS[@]}"; do
REPO_PATH="${REPOS_BASE}/${repo}"
echo "--- [${repo}] ---"

if [ ! -d "${REPO_PATH}/.git" ]; then
echo " SKIP: not a git repository (${REPO_PATH})"
continue
fi

pushd "${REPO_PATH}" > /dev/null

# 确保工作树干净
if [ -n "$(git status --porcelain)" ]; then
echo " SKIP: working tree is dirty"
FAILED+=("${repo} (dirty)")
popd > /dev/null
continue
fi

# 获取最新代码,确保标签落在最新远程状态上
if $REMOTE_PUSH; then
git fetch origin 2>/dev/null || true
fi

CURRENT_HASH=$(git rev-parse --short HEAD)
echo " Commit: ${CURRENT_HASH}"

if $DRY_RUN; then
echo " [DRY RUN] Would create: git tag -a ${TAG_NAME} -m \"${TAG_MESSAGE}\""
else
git tag -a "${TAG_NAME}" -m "${TAG_MESSAGE}"
echo " Created tag: ${TAG_NAME}"

if $REMOTE_PUSH; then
git push origin "${TAG_NAME}"
echo " Pushed to remote"
fi
fi

SUCCESS+=("${repo}")
popd > /dev/null
echo ""
done

# --- 摘要 ---
echo "=== Summary ==="
echo "Tag: ${TAG_NAME}"
echo "Message: ${TAG_MESSAGE}"
echo "Success: ${#SUCCESS[@]} repos"
for r in "${SUCCESS[@]}"; do
echo " + ${r}"
done
if [ ${#FAILED[@]} -gt 0 ]; then
echo "Failed: ${#FAILED[@]} repos"
for r in "${FAILED[@]}"; do
echo " - ${r}"
done
fi

3.3 用法示例


# 干运行,查看将打哪些标签
./tag-all.sh -d

# 本地创建标签
./tag-all.sh "Pre-release baseline: all modules pass sim"

# 创建并推送到 GitHub
./tag-all.sh -r "Fixed AXI deadlock in interconnect; added ILA to top"

# 使用自动生成的消息
./tag-all.sh

3.4 配套脚本:list-tags.sh


#!/bin/bash
# /home/user/tools/list-tags.sh
# 列出所有 FPGA 仓库中最近的快照标签。

REPOS_BASE="/home/user/work"
REPOS=("fpga_top" "fcam" "axi_interconnect" "video_pipeline" "ip_common")

for repo in "${REPOS[@]}"; do
REPO_PATH="${REPOS_BASE}/${repo}"
echo "=== ${repo} ==="
if [ -d "${REPO_PATH}/.git" ]; then
git -C "${REPO_PATH}" tag -l "snapshot-*" --sort=-creatordate --format=' %(refname:short) %(creatordate:short) %(subject)' | head -5
else
echo " (not found)"
fi
echo ""
done


4. GitHub 上标签与 Release 的关联

4.1 从标签创建 GitHub Release


# 使用 gh CLI(安装:sudo apt install gh)
gh release create snapshot-20260724153045 \
  --repo BuckHuang/fpga_top \
  --title "Snapshot 2026-07-24 15:30" \
  --notes "## Changes

- Added ILA debug hub with 8 probe ports
- Fixed Bayer-to-RGB pipeline stall
- Updated timing constraints for 200 MHz

### Affected repos
| Repo | Commit |
|------|--------|
| fpga_top | a1b2c3d |
| fcam | e4f5g6h |
| ip_common | i7j8k9l |" \
--prerelease

4.2 Release 附件

将构建输出附加到 Release 以保持可追溯性:


# 附加 FPGA 比特流和构建日志
gh release upload snapshot-20260724153045 \
  --repo BuckHuang/fpga_top \
  build/fpga_top.bit \
  build/timing_summary.rpt \
  build/vivado.log

4.3 查找给定标签对应的 Release


# 在 GitHub 上:https://github.com/BuckHuang/fpga_top/releases/tag/snapshot-20260724153045

# 通过 CLI:
gh release view snapshot-20260724153045 --repo BuckHuang/fpga_top


5. 标签回滚与删除策略

5.1 何时删除标签

场景 操作
标签创建在错误的提交上 删除本地和远程标签,重新创建
此快照的 CI 构建失败 删除标签(该标签从未有效)
标签消息包含敏感信息 删除并强制推送修正后的标签
旧快照(> 90 天),无关联 Release 可以安全清理

5.2 安全删除流程


# 1. 检查是否有 Release 使用此标签
gh release view snapshot-20260724153045 --repo BuckHuang/fpga_top
# 如果存在 Release,先删除 Release:
gh release delete snapshot-20260724153045 --repo BuckHuang/fpga_top --yes

# 2. 本地删除
git tag -d snapshot-20260724153045

# 3. 从远程删除
git push origin --delete snapshot-20260724153045
# 或:git push origin :refs/tags/snapshot-20260724153045

# 4. 验证删除
git ls-remote --tags origin | grep snapshot-20260724153045
# (应无输出)

5.3 批量清理旧快照


#!/bin/bash
# /home/user/tools/prune-tags.sh
# 删除所有仓库中超过 N 天的快照标签。
# 用法:prune-tags.sh 90  (删除超过 90 天的标签)

DAYS_OLD="${1:-90}"
CUTOFF_DATE=$(date -d "${DAYS_OLD} days ago" +%Y%m%d%H%M%S)
REPOS=("fpga_top" "fcam" "axi_interconnect" "video_pipeline" "ip_common")
REPOS_BASE="/home/user/work"

echo "Pruning snapshot tags older than ${DAYS_OLD} days (before ${CUTOFF_DATE})"
echo ""

for repo in "${REPOS[@]}"; do
REPO_PATH="${REPOS_BASE}/${repo}"
[ -d "${REPO_PATH}/.git" ] || continue

OLD_TAGS=$(git -C "${REPO_PATH}" tag -l "snapshot-*" | while read tag; do
TAG_DATE=${tag#snapshot-}
if [[ "${TAG_DATE}" < "${CUTOFF_DATE}" ]]; then
echo "$tag"
fi
done)

if [ -z "$OLD_TAGS" ]; then
echo "${repo}: nothing to prune"
continue
fi

echo "${repo}: found $(echo "$OLD_TAGS" | wc -l) old tags"
for tag in $OLD_TAGS; do
echo " Deleting: ${tag}"
git -C "${REPO_PATH}" tag -d "${tag}" 2>/dev/null || true
git -C "${REPO_PATH}" push origin --delete "${tag}" 2>/dev/null || true
done
done

5.4 恢复:如果误删了标签


# 如果知道标签指向的提交哈希:
git tag -a snapshot-20260724153045 <commit-hash> -m "Recovered tag"

# 如果不知道哈希,检查 reflog(仅限本地,90 天内):
git reflog | grep snapshot-20260724153045

# 检查是否有其他开发者的本地副本:
# 请团队成员执行:git push origin snapshot-20260724153045

5.5 策略总结

规则 详细说明
标签类型 始终使用附注标签(-a),绝不使用轻量标签
标签消息 包含 <Area>: <description>
创建频率 每个工作日至少一次;任何有风险的重构之前
推送策略 创建后立即推送标签(使用 -r 标志)
删除策略 仅在标签创建错误时删除;绝不删除已发布的标签
保留期限 无限期保留所有标签(磁盘成本可忽略不计)
清理 可选:清理超过 180 天且无关联 Release 的标签

6. 快速参考卡片


# 创建快照标签(单个仓库)
git tag -a snapshot-$(date +%Y%m%d%H%M%S) -m "fcam: description"

# 推送标签到远程
git push origin snapshot-20260724153045

# 列出最近的快照标签
git tag -l "snapshot-*" --sort=-creatordate | head -10

# 查看自上次标签以来的变更
git log --oneline $(git describe --abbrev=0)..HEAD

# 批量打标签(全部仓库,含推送)
/home/user/tools/tag-all.sh -r "message"

# 批量列出最近标签
/home/user/tools/list-tags.sh

# 清理旧标签
/home/user/tools/prune-tags.sh 180