> 统一管理多个 FPGA Git 仓库:批量拉取、推送、打标签和状态检查操作,带彩色输出。
独立管理 5 个 FPGA 仓库非常繁琐。多仓库管理系统提供以下能力:
/home/user/work/
├── FPGA_Prj/
│ ├── Project/
│ │ ├── Common/ # 共享 IP/vlib(仓库 1)
│ │ ├── Top/ # 顶层 FPGA 设计(仓库 2)
│ │ ├── WebServer/ # Web 服务器 RTL(仓库 3)
│ │ ├── Sensor/ # 传感器接口(仓库 4)
│ │ └── Camera/ # 摄像头流水线(仓库 5)
│ └── Doc/
│ └── lld/ # 底层设计文档
├── fpga_ila/ # FPGA ILA 项目(独立仓库)
└── tools/ # 构建工具和脚本
/home/user/.local/share/multi-repo/repos.conf
#!/bin/bash
# repos.conf -- 项目名称 → 路径映射
# 格式:DECLARE <项目短名称> <仓库路径> [<远程名称>]
# FPGA 核心仓库(5 个仓库)
DECLARE Common /home/user/work/FPGA_Prj/Project/Common BuckHuang
DECLARE Top /home/user/work/FPGA_Prj/Project/Top BuckHuang
DECLARE WebServer /home/user/work/FPGA_Prj/Project/WebServer BuckHuang
DECLARE Sensor /home/user/work/FPGA_Prj/Project/Sensor BuckHuang
DECLARE Camera /home/user/work/FPGA_Prj/Project/Camera BuckHuang
# 独立项目
DECLARE fpga_ila /home/user/work/fpga_ila BuckHuang
# 文档
DECLARE doc-lld /home/user/work/FPGA_Prj/Doc/lld BuckHuang
/home/user/.local/share/multi-repo/common.sh
#!/bin/bash
# common.sh -- 多仓库辅助函数
REPOS_CONF="/home/user/.local/share/multi-repo/repos.conf"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # 无颜色
BOLD='\033[1m'
# 从短名称获取仓库路径
# 用法:get_repo_path Common → /home/user/work/FPGA_Prj/Project/Common
get_repo_path() {
local name="$1"
source "$REPOS_CONF" 2>/dev/null
# 遍历已声明的仓库
while IFS= read -r line; do
if [[ "$line" =~ ^DECLARE[[:space:]]+([^[:space:]]+)[[:space:]]+(.+)$ ]]; then
if [ "${BASH_REMATCH[1]}" = "$name" ]; then
echo "${BASH_REMATCH[2]%% *}"
return 0
fi
fi
done < "$REPOS_CONF"
return 1
}
# 获取所有已声明的仓库路径
get_all_repos() {
while IFS= read -r line; do
if [[ "$line" =~ ^DECLARE[[:space:]]+([^[:space:]]+)[[:space:]]+(/[^[:space:]]+) ]]; then
echo "${BASH_REMATCH[2]} | ${BASH_REMATCH[1]}"
fi
done < "$REPOS_CONF"
}
# 打印带颜色的状态行
# 用法:print_status "OK" → 绿色 "OK"
# print_status "DIRTY" → 红色 "DIRTY"
print_status() {
local status="$1"
case "$status" in
OK|CLEAN|PASS) echo -e "${GREEN}${status}${NC}" ;;
DIRTY|FAIL|ERR) echo -e "${RED}${status}${NC}" ;;
BEHIND|AHEAD) echo -e "${YELLOW}${status}${NC}" ;;
*) echo -e "${NC}${status}${NC}" ;;
esac
}
文件:/home/user/.local/bin/pull-all.sh
#!/bin/bash
# pull-all.sh -- 拉取所有 FPGA 仓库
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "/home/user/.local/share/multi-repo/common.sh"
PULL_ERRORS=()
SUCCESS_COUNT=0
FAIL_COUNT=0
TOTAL=0
echo -e "${BOLD}=== 正在拉取所有仓库 ===${NC}"
echo ""
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
((TOTAL++))
if [ ! -d "$repo_path" ]; then
echo -e " ${MAGENTA}[${repo_name}]${NC} ${RED}跳过${NC}(目录不存在:$repo_path)"
((FAIL_COUNT++))
PULL_ERRORS+=("${repo_name}:目录不存在")
continue
fi
printf " ${MAGENTA}[%-12s]${NC} " "$repo_name"
pushd "$repo_path" > /dev/null || continue
# 获取当前分支
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$branch" ]; then
popd > /dev/null
echo -e "${RED}失败${NC}(不是 git 仓库)"
((FAIL_COUNT++))
PULL_ERRORS+=("${repo_name}:不是 git 仓库")
continue
fi
# 检查未提交的更改
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
echo -e "${YELLOW}有未提交更改${NC}(分支 $branch 上有未提交更改,继续拉取)"
fi
# 执行拉取
pull_output=$(git pull --ff-only 2>&1)
pull_rc=$?
if [ $pull_rc -eq 0 ]; then
# 检查是否确实有更新
if echo "$pull_output" | grep -q "Already up to date"; then
echo -e "${GREEN}已是最新${NC}($branch)"
else
echo -e "${GREEN}已拉取${NC}($branch)← 已更新"
fi
((SUCCESS_COUNT++))
else
echo -e "${RED}失败${NC}($branch)"
echo " ${RED}→${NC} $(echo "$pull_output" | tail -1)"
((FAIL_COUNT++))
PULL_ERRORS+=("${repo_name}:$pull_output")
fi
popd > /dev/null
done < <(get_all_repos)
# 汇总
echo ""
echo -e "${BOLD}=== 拉取汇总 ===${NC}"
echo -e " 总计: ${TOTAL}"
echo -e " ${GREEN}成功:${NC} ${SUCCESS_COUNT}"
echo -e " ${RED}失败:${NC} ${FAIL_COUNT}"
if [ ${#PULL_ERRORS[@]} -gt 0 ]; then
echo ""
echo -e "${BOLD}${RED}错误详情:${NC}"
for err in "${PULL_ERRORS[@]}"; do
echo -e " ${RED}✗${NC} $err"
done
exit 1
fi
echo ""
echo -e "${GREEN}所有仓库已是最新。${NC}"
exit 0
chmod +x /home/user/.local/bin/pull-all.sh
文件:/home/user/.local/bin/push-all.sh
#!/bin/bash
# push-all.sh -- 推送所有有未提交更改的 FPGA 仓库
source "/home/user/.local/share/multi-repo/common.sh"
COMMIT_MSG="${1:-}"
PUSH_TAGS="${2:-}" # 传入 "tags" 以同时推送标签
echo -e "${BOLD}=== 正在推送所有仓库 ===${NC}"
echo ""
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
[ ! -d "$repo_path/.git" ] && continue
printf " ${MAGENTA}[%-12s]${NC} " "$repo_name"
pushd "$repo_path" > /dev/null
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
# 检查是否有需要推送的提交
ahead=$(git rev-list --count origin/$branch..$branch 2>/dev/null)
dirty_flag=""
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
dirty_flag=" [有未提交更改]"
fi
if [ "$ahead" -gt 0 ] 2>/dev/null; then
git push origin "$branch" 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}已推送${NC}($branch,领先 $ahead 个提交$dirty_flag)"
# 可选:同时推送标签
if [ "$PUSH_TAGS" = "tags" ]; then
git push origin --tags 2>/dev/null
fi
else
echo -e "${RED}被拒绝${NC}(可能需要先拉取)"
fi
elif [ -n "$dirty_flag" ]; then
echo -e "${YELLOW}跳过${NC}(有未提交更改,无新提交)"
else
echo -e "${GREEN}已是最新${NC}($branch)"
fi
popd > /dev/null
done < <(get_all_repos)
echo ""
echo -e "${GREEN}推送完成。${NC}"
chmod +x /home/user/.local/bin/push-all.sh
文件:/home/user/.local/bin/tag-all.sh
#!/bin/bash
# tag-all.sh -- 在所有仓库中创建快照标签
# 用法:tag-all.sh [标签名]
# 如果不提供标签名,自动生成:snapshot-YYYYMMDDHHmmss
source "/home/user/.local/share/multi-repo/common.sh"
if [ -z "$1" ]; then
TAG="snapshot-$(date +%Y%m%d%H%M%S)"
else
TAG="$1"
fi
TAG_MESSAGE="${2:-"快照标签:$TAG"}"
echo -e "${BOLD}=== 正在创建标签:${CYAN}$TAG${NC} ${BOLD}===${NC}"
echo ""
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
[ ! -d "$repo_path/.git" ] && continue
printf " ${MAGENTA}[%-12s]${NC} " "$repo_name"
pushd "$repo_path" > /dev/null
# 检查标签是否已存在
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo -e "${YELLOW}已存在${NC}(标签已存在)"
popd > /dev/null
continue
fi
# 创建附注标签
if git tag -a "$TAG" -m "$TAG_MESSAGE" 2>/dev/null; then
echo -e "${GREEN}已打标签${NC}"
else
echo -e "${RED}失败${NC}(无法创建标签)"
fi
popd > /dev/null
done < <(get_all_repos)
echo ""
echo -e "${BOLD}推送标签:${NC} tag-all.sh $TAG push"
echo -e "或手动执行: git push origin $TAG (在每个仓库中)"
# 处理推送标志
if [ "$3" = "push" ]; then
echo ""
echo -e "${BOLD}正在推送标签...${NC}"
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
[ ! -d "$repo_path/.git" ] && continue
pushd "$repo_path" > /dev/null
git push origin "$TAG" 2>/dev/null && \
echo -e " ${GREEN}✓${NC} $repo_name" || \
echo -e " ${RED}✗${NC} $repo_name"
popd > /dev/null
done < <(get_all_repos)
fi
chmod +x /home/user/.local/bin/tag-all.sh
文件:/home/user/.local/bin/status-all.sh
#!/bin/bash
# status-all.sh -- 所有仓库的统一状态总览
source "/home/user/.local/share/multi-repo/common.sh"
echo -e "${BOLD}=== 仓库状态总览 ===${NC}"
echo -e "${BOLD}$(date '+%Y-%m-%d %H:%M:%S')${NC}"
echo ""
# 表头
printf " ${BOLD}%-14s %-20s %-8s %-6s %-6s %s${NC}\n" \
"项目" "分支" "状态" "领先" "落后" "暂存"
printf " %-14s %-20s %-8s %-6s %-6s %s\n" \
"──────" "──────" "──────" "─────" "──────" "─────"
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
if [ ! -d "$repo_path/.git" ]; then
printf " ${MAGENTA}%-14s${NC} ${RED}%-20s${NC}\n" "$repo_name" "不是仓库"
continue
fi
pushd "$repo_path" > /dev/null
# 分支
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$branch" ]; then
branch="(分离头指针)"
fi
# 工作树状态
if git diff-index --quiet HEAD -- 2>/dev/null; then
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
wstatus="${YELLOW}未跟踪${NC}"
else
wstatus="${GREEN}干净${NC}"
fi
else
# 统计修改的文件数
mod_count=$(git diff --name-only | wc -l)
stage_count=$(git diff --cached --name-only | wc -l)
wstatus="${RED}有更改${NC}"
if [ "$stage_count" -gt 0 ]; then
wstatus="${RED}有更改(${stage_count}已暂存)${NC}"
fi
fi
# 领先/落后提交数
ahead=$(git rev-list --count origin/$branch..$branch 2>/dev/null || echo "?")
behind=$(git rev-list --count $branch..origin/$branch 2>/dev/null || echo "?")
if [ "$ahead" = "?" ] || [ "$behind" = "?" ]; then
ahead="?"
behind="?"
fi
# 暂存区数量
stash_count=$(git stash list 2>/dev/null | wc -l)
stash_display="$stash_count"
if [ "$stash_count" -gt 0 ]; then
stash_display="${YELLOW}${stash_count}${NC}"
fi
# 打印行
printf " ${MAGENTA}%-14s${NC} %-20s " "$repo_name" "${branch:0:19}"
# 带颜色的领先/落后显示
if [ "$ahead" != "?" ] && [ "$ahead" -gt 0 ]; then
printf "${YELLOW}%-8s${NC}" "↑${ahead}"
else
printf "%-8s" "${wstatus}"
fi
if [ "$ahead" != "?" ] && [ "$ahead" -gt 0 ]; then
printf "${YELLOW}%-6s${NC}" "$ahead"
else
printf "%-6s" "-"
fi
if [ "$behind" != "?" ] && [ "$behind" -gt 0 ]; then
printf "${RED}%-6s${NC}" "$behind"
else
printf "%-6s" "-"
fi
printf "%s\n" "$stash_display"
popd > /dev/null
done < <(get_all_repos)
echo ""
echo -e " ${GREEN}干净${NC}=无更改 ${RED}有更改${NC}=未提交 ${YELLOW}未跟踪${NC}=有新文件"
echo -e " ↑N = 领先 N 个提交 ↓N = 落后 N 个提交"
chmod +x /home/user/.local/bin/status-all.sh
| 状态 | 颜色 | 含义 |
|---|---|---|
干净(CLEAN) |
绿色 | 工作树干净,与远程同步 |
已是最新(UP-TO-DATE) |
绿色 | 没有需要拉取的新更改 |
已拉取(PULLED) |
绿色 | 成功拉取了新提交 |
已推送(PUSHED) |
绿色 | 成功推送了本地提交 |
有更改(DIRTY) |
红色 | 工作树有未提交的更改 |
领先(AHEAD) |
黄色 | 有尚未推送的本地提交 |
落后(BEHIND) |
红色 | 远程有尚未拉取的提交 |
未跟踪(UNTRACK) |
黄色 | 存在新的未跟踪文件 |
跳过(SKIP) |
黄色 | 操作被跳过(例如:无内容可推送) |
失败(FAIL) |
红色 | 操作失败 |
已存在(EXISTS) |
黄色 | 标签已存在 |
# 在脚本中,在每个批量脚本顶部定义这些颜色变量
RED='\033[0;31m' # 错误、有更改、失败
GREEN='\033[0;32m' # 成功、干净、正常
YELLOW='\033[1;33m' # 警告、领先、落后
BLUE='\033[0;34m' # 信息
MAGENTA='\033[0;35m' # 项目名称
CYAN='\033[0;36m' # 标签名、URL
NC='\033[0m' # 重置
BOLD='\033[1m' # 标题
=== 仓库状态总览 ===
2026-07-24 14:35:12
项目 分支 状态 领先 落后 暂存
────── ────── ────── ───── ────── ─────
Common main 干净 - - 0
Top dev/fix-timing 有更改 ↑2 - 1
WebServer main 已是最新 - ↓3 0
Sensor feature/spi 有更改 ↑5 - 0
Camera main 干净 - - 2
干净=无更改 有更改=未提交 未跟踪=有新文件
↑N = 领先 N 个提交 ↓N = 落后 N 个提交
popd,即使操作失败(使用 trap EXIT 模式)
#!/bin/bash
# robust-batch-op.sh -- 容错的批量操作模板
source "/home/user/.local/share/multi-repo/common.sh"
ERRORS=()
SUCCESS=0
TOTAL=0
batch_operation() {
local repo_path="$1"
local repo_name="$2"
# 守卫 1:目录是否存在
if [ ! -d "$repo_path" ]; then
return 1
fi
# 守卫 2:是否是 git 仓库
if [ ! -d "$repo_path/.git" ]; then
return 2
fi
pushd "$repo_path" > /dev/null || return 3
# 守卫 3:能否解析分支名
local branch
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$branch" ]; then
popd > /dev/null
return 4
fi
# === 在此处编写你的操作 ===
# git pull --ff-only || return 5
# git push || return 6
# 等等。
popd > /dev/null
return 0
}
while IFS='|' read -r repo_path repo_name; do
repo_name=$(echo "$repo_name" | xargs)
((TOTAL++))
printf " ${MAGENTA}[%-12s]${NC} " "$repo_name"
batch_operation "$repo_path" "$repo_name"
rc=$?
case $rc in
0) echo -e "${GREEN}正常${NC}"; ((SUCCESS++)) ;;
1) echo -e "${RED}缺失${NC}(目录不存在)"; ERRORS+=("$repo_name:目录不存在") ;;
2) echo -e "${RED}非 GIT${NC}"; ERRORS+=("$repo_name:不是 git 仓库") ;;
3) echo -e "${RED}CHDIR${NC}"; ERRORS+=("$repo_name:cd 失败") ;;
4) echo -e "${RED}分离头指针${NC}"; ERRORS+=("$repo_name:分离 HEAD") ;;
*) echo -e "${RED}失败($rc)${NC}"; ERRORS+=("$repo_name:退出码 $rc") ;;
esac
done < <(get_all_repos)
# 最终汇总
echo ""
echo "总计:$TOTAL | 成功:$SUCCESS | 失败:$((TOTAL - SUCCESS))"
if [ ${#ERRORS[@]} -gt 0 ]; then
echo ""
echo "错误详情:"
for e in "${ERRORS[@]}"; do
echo " ✗ $e"
done
exit 1
fi
exit 0
# 暂存更改、拉取、然后恢复暂存
git stash
git pull --ff-only
git stash pop
# 如有冲突则手动解决
场景 2:分支分叉(需要合并)
# 检查发生了什么
git log --oneline --graph --all -10
# 选项 A:变基
git pull --rebase
# 选项 B:合并
git pull --no-ff
# 选项 C:重置(危险:会丢弃本地更改)
git reset --hard origin/main
场景 3:标签已存在(指向不同的提交)
# 查看标签指向哪里
git show snapshot-20260724
# 强制移动标签(谨慎操作)
git tag -af snapshot-20260724143512 -m "更新后的快照"
git push origin snapshot-20260724143512 --force
| Skill 名称 | 批量脚本 | 用途 |
|---|---|---|
git-pull |
pull-all.sh |
拉取指定项目或所有项目 |
git-push |
push-all.sh |
推送指定项目或所有项目 |
git-list |
status-all.sh |
显示详细的版本历史 |
Claude skill(如 git-pull)接受项目名称作为参数:
#!/bin/bash
# skill-gateway.sh -- 由 Claude Code skill 调用
# 用法:skill-gateway.sh git-pull Camera → 仅拉取 Camera
# skill-gateway.sh git-pull --all → 拉取所有仓库
SKILL="$1"
PROJECT="$2"
source "/home/user/.local/share/multi-repo/common.sh"
case "$SKILL" in
git-pull)
if [ "$PROJECT" = "--all" ]; then
/home/user/.local/bin/pull-all.sh
else
repo_path=$(get_repo_path "$PROJECT")
if [ -z "$repo_path" ]; then
echo "未知项目:$PROJECT"
exit 1
fi
cd "$repo_path" && git pull --ff-only
fi
;;
git-push)
if [ "$PROJECT" = "--all" ]; then
/home/user/.local/bin/push-all.sh
else
repo_path=$(get_repo_path "$PROJECT")
cd "$repo_path" && git push
fi
;;
*)
echo "未知 skill:$SKILL"
exit 1
;;
esac
# 创建目录
mkdir -p /home/user/.local/share/multi-repo
mkdir -p /home/user/.local/bin
# 确保 ~/.local/bin 在 PATH 中
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/user/.bashrc
source /home/user/.bashrc
# 验证
which pull-all.sh
pull-all.sh
status-all.sh