Git 多仓库批量管理

> 统一管理多个 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
}


pull-all.sh 实现

文件:/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


批量操作

push-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

tag-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

status-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) 黄色 标签已存在

ANSI 颜色参考


# 在脚本中,在每个批量脚本顶部定义这些颜色变量
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 个提交


错误处理与部分故障恢复

原则

稳健的仓库迭代模式


#!/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

恢复策略

场景 1:由于本地有更改导致拉取被拒绝

# 暂存更改、拉取、然后恢复暂存
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


与 Claude Skills 集成

Claude Skill 与批量脚本的映射关系

Skill 名称 批量脚本 用途
git-pull pull-all.sh 拉取指定项目或所有项目
git-push push-all.sh 推送指定项目或所有项目
git-list status-all.sh 显示详细的版本历史

Skill 网关模式

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