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

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

相关推荐
Leo.yuan42 分钟前
数据建模怎么做?一文解析8种经典数据建模方法
大数据·数学建模
用什么都重名1 小时前
Git 合并两个无共同历史的分支:从报错到解决全记录
git·gitlab
果丁智能1 小时前
物联网智能锁落地实践:破解网约房、民宿身份核验与远程权限管控难题
大数据·人工智能·物联网·智能家居
搜移IT科技1 小时前
全球供应链重构凸显制造优势,非金属材料板块出口景气度与海外拓展策略
大数据
中科岩创1 小时前
某景区地下隧道结构健康监测工程项目
大数据·物联网·自动化
2601_961875241 小时前
花生十三资料网盘|百度云|下载
数据库·windows·git·svn·eclipse·github
汉知宝科技1 小时前
企业知识产权管理的数据安全与部署策略:从双模式架构到精细化管控
大数据·运维
Volunteer Technology1 小时前
Flink 时间、窗口及操作(三)
大数据·flink
招标采购导航网1 小时前
标讯类目体系的自动演化:招标采购导航网如何根据新出现的行业自动扩展分类
大数据·运维·人工智能