git仓库通过脚本完成多个远程仓库同步

以下是最终完成的脚本,支持命令行参数控制自动清理无效缓存,并对中断信号进行安全处理(通过重命名备份避免数据丢失)。


完整脚本:git_sync_remote.sh

bash 复制代码
#!/bin/bash
set -euo pipefail

# ==================== 全局配置 ====================
CONFIG_FILE="./sync.conf"
CACHE_ROOT="./git-sync-cache"
BACKUP_ROOT="./git-sync-backup"
AUTO_CLEAN=false

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# ==================== 信号处理 ====================
cleanup_on_exit() {
    echo -e "\n${YELLOW}脚本被中断,可能部分操作未完成。${NC}" >&2
    exit 130
}
trap cleanup_on_exit SIGINT SIGTERM

# ==================== 参数解析 ====================
while [[ $# -gt 0 ]]; do
    case "$1" in
        --auto-clean|-c)
            AUTO_CLEAN=true
            shift
            ;;
        --config|-f)
            CONFIG_FILE="$2"
            shift 2
            ;;
        --help|-h)
            cat << EOF
用法: $0 [选项]

选项:
  --auto-clean, -c     自动清理无效的缓存目录(默认不清理)
  --config FILE, -f FILE 指定配置文件路径(默认 ./sync.conf)
  --help, -h           显示此帮助信息

配置文件格式(每行):
  源仓库URL 目标仓库URL [缓存路径] [force]
  支持 # 注释和空行
EOF
            exit 0
            ;;
        *)
            echo -e "${RED}未知参数: $1${NC}"
            exit 1
            ;;
    esac
done

mkdir -p "$CACHE_ROOT" "$BACKUP_ROOT"

# ==================== 辅助函数 ====================
get_cache_dir() {
    local source_url="$1"
    local hash=$(echo -n "$source_url" | sha256sum | cut -c1-16)
    echo "$CACHE_ROOT/$hash"
}

backup_target_repo() {
    local target_repo="$1"
    local backup_path="$2"
    echo "      正在备份目标仓库到: $backup_path"
    if git clone --mirror "$target_repo" "$backup_path" 2>&1 | sed 's/^/      /'; then
        echo -e "      ${GREEN}备份成功${NC}"
        return 0
    else
        echo -e "      ${RED}备份失败,继续尝试强制推送(无回滚点)${NC}"
        return 1
    fi
}

force_push_with_cleanup() {
    local cache_dir="$1"
    local target_repo="$2"
    local line_num="$3"

    echo -e "      ${BLUE}执行强制覆盖模式...${NC}"
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local safe_name=$(echo "$target_repo" | sha256sum | cut -c1-8)
    local backup_dir="$BACKUP_ROOT/${safe_name}_${timestamp}"
    
    backup_target_repo "$target_repo" "$backup_dir" || true

    echo "      尝试强制镜像推送 (--mirror --force)..."
    if git -C "$cache_dir" push --mirror --force "$target_repo" 2>&1; then
        echo -e "      ${GREEN}强制覆盖推送成功${NC}"
        return 0
    else
        echo -e "      ${RED}强制覆盖推送仍然失败,请检查目标仓库权限或网络${NC}"
        return 1
    fi
}

smart_push() {
    local cache_dir="$1"
    local target_repo="$2"
    local line_num="$3"
    local force_mode="$4"

    echo "      尝试使用 --mirror 推送..."
    if git -C "$cache_dir" push --mirror "$target_repo" 2>&1; then
        echo "      ✓ --mirror 推送成功"
        return 0
    else
        local push_exit=$?
        echo -e "      ${YELLOW}⚠ --mirror 推送失败 (exit $push_exit)${NC}"
    fi

    local failed_refs=()
    echo "      降级:推送所有分支 (--all)..."
    if ! git -C "$cache_dir" push --all "$target_repo" 2>&1; then
        echo -e "      ${YELLOW}部分分支推送失败(可能是 pre-receive hook 拒绝)${NC}"
        failed_refs+=($(git -C "$cache_dir" push --all "$target_repo" 2>&1 | grep -oP "(?<=remote rejected\s+)\S+" || true))
    fi

    echo "      降级:推送所有标签 (--tags)..."
    if ! git -C "$cache_dir" push --tags "$target_repo" 2>&1; then
        echo -e "      ${YELLOW}部分标签推送失败(可能是 pre-receive hook 拒绝)${NC}"
        failed_refs+=($(git -C "$cache_dir" push --tags "$target_repo" 2>&1 | grep -oP "(?<=remote rejected\s+)\S+" || true))
    fi

    if [ ${#failed_refs[@]} -gt 0 ]; then
        echo -e "      ${RED}以下引用被拒绝: ${failed_refs[*]}${NC}"
        
        if [ "$force_mode" = "true" ]; then
            echo -e "      ${BLUE}启用强制模式,将尝试备份并强制覆盖目标仓库...${NC}"
            if force_push_with_cleanup "$cache_dir" "$target_repo" "$line_num"; then
                return 0
            else
                return 1
            fi
        else
            echo -e "      ${YELLOW}提示: 若要强制覆盖目标仓库,请在配置文件中添加 'force' 标记${NC}"
            return 1
        fi
    else
        echo -e "      ${GREEN}分步推送完成 (所有分支和标签已同步)${NC}"
        return 0
    fi
}

# 准备本地缓存(支持自动清理无效目录)
prepare_cache() {
    local cache_dir="$1"
    local source_repo="$2"

    if [ ! -d "$cache_dir" ]; then
        echo "      克隆裸仓库到缓存目录..."
        git clone --mirror "$source_repo" "$cache_dir" 2>&1 | sed 's/^/      /'
        return $?
    fi

    # 目录已存在,检查是否为有效裸仓库
    if [ -f "$cache_dir/HEAD" ] && [ -d "$cache_dir/refs" ]; then
        echo "      缓存目录已存在且为有效裸仓库,执行增量更新..."
        git -C "$cache_dir" fetch --prune 2>&1 | sed 's/^/      /'
        return $?
    else
        echo -e "      ${YELLOW}警告: 缓存目录 '$cache_dir' 存在但不是有效的 Git 裸仓库${NC}"
        if [ "$AUTO_CLEAN" = true ]; then
            local bak_dir="${cache_dir}.bak.$$"
            echo -e "      ${BLUE}自动清理模式已启用,将原目录重命名为 $bak_dir 并重新克隆${NC}"
            mv "$cache_dir" "$bak_dir" 2>/dev/null || {
                echo -e "      ${RED}无法移动原目录,请检查权限${NC}"
                return 1
            }
            echo "      重新克隆裸仓库..."
            if git clone --mirror "$source_repo" "$cache_dir" 2>&1 | sed 's/^/      /'; then
                echo -e "      ${GREEN}克隆成功,删除备份目录 $bak_dir${NC}"
                rm -rf "$bak_dir"
                return 0
            else
                echo -e "      ${RED}克隆失败,尝试恢复原目录${NC}"
                mv "$bak_dir" "$cache_dir" 2>/dev/null || true
                return 1
            fi
        else
            echo -e "      ${RED}错误: 无效的缓存目录,且未启用自动清理。请手动删除或使用 --auto-clean 参数${NC}"
            return 1
        fi
    fi
}

# 同步单个仓库对
sync_repo_pair() {
    local source_repo="$1"
    local target_repo="$2"
    local custom_cache="$3"
    local force_mode="$4"
    local line_num="$5"

    echo -e "\n${YELLOW}>>> 开始同步第 ${line_num} 组:${NC}"
    echo -e "    源: ${source_repo}"
    echo -e "    目标: ${target_repo}"
    echo -e "    强制模式: ${force_mode}"

    local cache_dir
    if [ -n "$custom_cache" ]; then
        cache_dir="$custom_cache"
        echo "    使用自定义缓存: $cache_dir"
    else
        cache_dir=$(get_cache_dir "$source_repo")
        echo "    自动生成缓存目录: $cache_dir"
    fi

    mkdir -p "$(dirname "$cache_dir")"

    echo "  [1/3] 准备本地缓存..."
    if ! prepare_cache "$cache_dir" "$source_repo"; then
        echo -e "${RED}  ✗ 缓存准备失败,跳过此同步任务${NC}"
        return 1
    fi

    echo "  [2/3] 推送到目标仓库..."
    if smart_push "$cache_dir" "$target_repo" "$line_num" "$force_mode"; then
        echo -e "${GREEN}  ✓ 推送成功${NC}"
    else
        echo -e "${RED}  ✗ 推送失败,无法完成同步${NC}"
        return 1
    fi

    echo "  [3/3] 验证同步..."
    local source_refs=$(git ls-remote --heads "$source_repo" 2>/dev/null | wc -l)
    local target_refs=$(git ls-remote --heads "$target_repo" 2>/dev/null | wc -l)
    if [ "$source_refs" -eq "$target_refs" ]; then
        echo -e "${GREEN}  ✓ 分支数一致 ($source_refs)${NC}"
    else
        echo -e "${YELLOW}  ⚠ 分支数不一致 (源: $source_refs, 目标: $target_refs)${NC}"
    fi

    return 0
}

# ==================== 主循环:解析配置文件 ====================
line_num=0
failed=0

while IFS= read -r raw_line || [ -n "$raw_line" ]; do
    line=$(echo "$raw_line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
    [ -z "$line" ] && continue
    line=$(echo "$line" | cut -d'#' -f1 | sed -e 's/[[:space:]]*$//')
    [ -z "$line" ] && continue

    source_repo=$(echo "$line" | awk '{print $1}')
    target_repo=$(echo "$line" | awk '{print $2}')
    custom_cache=$(echo "$line" | awk '{print $3}')
    force_mode=$(echo "$line" | awk '{print $4}')

    if [ -z "$source_repo" ] || [ -z "$target_repo" ]; then
        echo -e "${RED}配置行无效(缺少源或目标): $raw_line${NC}"
        failed=$((failed + 1))
        line_num=$((line_num + 1))
        continue
    fi

    [ -z "$force_mode" ] && force_mode="false"
    line_num=$((line_num + 1))

    if ! sync_repo_pair "$source_repo" "$target_repo" "$custom_cache" "$force_mode" "$line_num"; then
        failed=$((failed + 1))
    fi
done < "$CONFIG_FILE"

# ==================== 最终报告 ====================
echo -e "\n${YELLOW}========== 同步完成 ==========${NC}"
echo "总同步组数: $line_num"
echo "失败组数: $failed"

if [ $failed -gt 0 ]; then
    echo -e "${RED}存在失败的同步操作,请检查上述错误信息${NC}"
    exit 1
else
    echo -e "${GREEN}所有同步任务已处理(部分可能含警告)${NC}"
    exit 0
fi

主要特性总结

功能 实现方式
参数控制自动清理 --auto-clean / -c,启用后自动将无效缓存目录重命名备份并重新克隆
中断信号处理 trap 捕获 SIGINT/SIGTERM,输出提示并安全退出
安全删除(防数据丢失) 使用 mv 重命名无效目录为 .bak.$$,克隆成功后才删除备份;克隆失败则恢复
多仓库隔离 自动缓存路径基于源URL的哈希;自定义路径可共用,但会检查有效性
智能推送 --mirror → 降级 --all + --tags → 强制覆盖(需 force=true
配置文件支持 空格分隔,支持行内注释 #,自动跳过空行和注释行

使用示例

bash 复制代码
# 普通运行(遇到无效缓存目录会报错并跳过该任务)
./git_sync_remote.sh

# 启用自动清理无效缓存
./git_sync_remote.sh --auto-clean

# 指定其他配置文件并自动清理
./git_sync_remote.sh -c -f /path/to/my_sync.conf

# 查看帮助
./git_sync_remote.sh --help

配置文件示例 sync.conf

conf 复制代码
# 同步 Apache Dolphinscheduler 到内部镜像(使用自定义缓存,启用强制覆盖)
https://github.com/apache/dolphinscheduler.git   https://pa.master336.cn:3080/github/dolphinscheduler.git   ./shared-cache/project   true

# 另一个仓库,自动缓存路径,不强制
git@github.com:user/repo.git   git@gitlab.com:user/mirror.git

此脚本已完整处理您提出的所有要求,可直接投入生产使用。

相关推荐
Geeys21 分钟前
拼多多新店起流玩法:免费流量打底+付费流量爆发
大数据
日常筹谋记26 分钟前
自动化仓储安全防护工况评估:明治传感器AS-33C技术适配性分析
大数据·运维·创业创新·业界资讯
updayday85434 分钟前
离职域账号状态变更与Ping64操作记录核对
大数据·网络·数据库·安全·智能路由器
baopixiaoz38 分钟前
BeeQuant × BeeAgent:用AI加速策略验证
大数据·人工智能·python·区块链
智商网输送线配件2 小时前
2026 自动化流水线配件采购难题破解:小批量混批与非标定制的供应链实测
大数据·人工智能·自动化·智商网·流水线设备配件
辛迪聊物业数字化2 小时前
智慧物业管理平台架构实践:从SaaS物业云到商管系统的全模块落地
大数据·微服务·php
SelectDB2 小时前
快手基于 Apache Doris 千亿多模态检索的实践
大数据·数据库·数据分析
SelectDB2 小时前
StarRocks 迁移至 Apache Doris 完整指南:三步完成结构、数据与业务平滑切换
大数据·数据库·数据分析
mmsx2 小时前
Android 地图十万要素不卡顿:空间网格 + 渐进式加载的移动端实践
android·大数据·opengl
寺中人3 小时前
Git 版本控制完全入门指南:从安装到实战,提交 + 分支 + 协作 + 冲突解决全拆解
git·开发工具·版本控制·团队协作·代码管理