Git 仓库批量拉取最新代码脚本

老牛浏览 28评论 0发表于 更新于

电脑上项目比较多,经常会忘记拉取最新代码,导致时不时就会有冲突,因此就有了这个脚本,批量同步拉取,支持 Linux/macOS。

一、批量拉取脚本

pull_repos

bash
#!/bin/bash

# Git仓库批量同步脚本 - 跨平台兼容版本(Linux/macOS)

# ============ 配置区域 ============
CONFIG_FILE="${1:-pull_repos.conf}"  # 配置文件路径
FORCE_RESET=false
DEPTH=""
# =================================

# 设置颜色输出(检查是否支持颜色)
if [ -t 1 ]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    BLUE='\033[0;34m'
    CYAN='\033[0;36m'
    NC='\033[0m'
else
    RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; NC=''
fi

show_help() {
    echo "用法: $0 [配置文件] [选项]"
    echo ""
    echo "参数:"
    echo "  配置文件    指定仓库列表文件(默认: pull_repos.conf)"
    echo ""
    echo "选项:"
    echo "  -h, --help     显示此帮助信息"
    echo "  -d, --depth N  限制git pull的深度"
    echo "  -f, --force    强制重置本地更改"
    echo "  -q, --quiet    静默模式,减少输出"
    echo ""
    echo "配置文件格式:"
    echo "  每行一个目录路径,支持相对路径和绝对路径"
    echo "  以 # 开头的行是注释"
    echo "  支持指定分支: 目录路径 分支名"
    echo ""
    echo "示例:"
    echo "  $0                      # 使用默认配置文件"
    echo "  $0 my-repos.txt         # 使用自定义配置文件"
    echo "  $0 -f                   # 强制重置模式"
    echo "  $0 --depth 1 repos.txt  # 浅克隆模式"
    echo ""
}

# 清理行内容:移除各种换行符和首尾空白(跨平台兼容)
clean_line() {
    local line="$1"
    # 移除 \r (Windows/Mac 换行符)
    line=$(printf "%s" "$line" | tr -d '\r')
    # 移除首尾空格和制表符
    line=$(printf "%s" "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
    printf "%s" "$line"
}

# 检查行是否有效(非空且非注释)
is_valid_line() {
    local line="$1"
    local cleaned
    
    cleaned=$(clean_line "$line")
    
    # 空行无效
    if [ -z "$cleaned" ]; then
        return 1
    fi
    
    # 注释行无效
    case "$cleaned" in
        \#*)
            return 1
            ;;
    esac
    
    return 0
}

# 读取所有有效行到全局数组(跨平台兼容)
read_valid_lines() {
    local file="$1"
    
    # 清空全局数组
    REPO_LINES=()
    
    # 检查文件是否存在且可读
    if [ ! -f "$file" ] || [ ! -r "$file" ]; then
        echo -e "${RED}错误: 无法读取配置文件 '$file'${NC}" >&2
        return 1
    fi
    
    # 使用 cat 和 while 循环读取每一行(兼容所有平台)
    while IFS= read -r line || [ -n "$line" ]; do
        if is_valid_line "$line"; then
            cleaned=$(clean_line "$line")
            REPO_LINES+=("$cleaned")
        fi
    done < "$file"
}

# 检测文件换行符类型(跨平台兼容)
detect_line_endings() {
    local file="$1"
    
    # 使用 od 或 hexdump 检测(两种都支持)
    if command -v od >/dev/null 2>&1; then
        if od -c "$file" | grep -q "\\\\r\\\\n"; then
            echo -e "${YELLOW}检测到 Windows 格式 (CRLF)${NC}"
        elif od -c "$file" | grep -q "\\\\r"; then
            echo -e "${YELLOW}检测到 Mac 格式 (CR)${NC}"
        else
            echo -e "${GREEN}检测到 Unix 格式 (LF)${NC}"
        fi
    else
        # 简单检测
        if grep -q $'\r\n' "$file" 2>/dev/null; then
            echo -e "${YELLOW}检测到 Windows 格式 (CRLF)${NC}"
        elif grep -q $'\r' "$file" 2>/dev/null; then
            echo -e "${YELLOW}检测到 Mac 格式 (CR)${NC}"
        else
            echo -e "${GREEN}检测到 Unix 格式 (LF)${NC}"
        fi
    fi
}

# 解析命令行参数
QUIET=false
args=()
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_help
            exit 0
            ;;
        -f|--force)
            FORCE_RESET=true
            shift
            ;;
        -d|--depth)
            DEPTH="--depth $2"
            shift 2
            ;;
        -q|--quiet)
            QUIET=true
            shift
            ;;
        -*)
            echo "未知选项: $1"
            show_help
            exit 1
            ;;
        *)
            args+=("$1")
            shift
            ;;
    esac
done

# 如果提供了第一个非选项参数,作为配置文件
if [ ${#args[@]} -gt 0 ]; then
    CONFIG_FILE="${args[0]}"
fi

# 检查配置文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
    echo -e "${RED}错误: 配置文件 '$CONFIG_FILE' 不存在${NC}"
    echo "请创建配置文件,每行一个目录路径"
    echo ""
    echo "示例配置文件内容:"
    echo "  # 这是我的仓库列表"
    echo "  project1"
    echo "  project2"
    echo "  ../other-project"
    exit 1
fi

# 显示配置信息(非静默模式)
if [ "$QUIET" = false ]; then
    echo -e "${BLUE}配置文件:${NC} $CONFIG_FILE"
    echo -e "$(detect_line_endings "$CONFIG_FILE")"
    echo ""
fi

# 读取有效行
REPO_LINES=()
read_valid_lines "$CONFIG_FILE"

TOTAL_COUNT=${#REPO_LINES[@]}

if [ "$QUIET" = false ]; then
    echo -e "${GREEN}使用配置文件: $CONFIG_FILE${NC}"
    echo -e "${BLUE}共发现 ${TOTAL_COUNT} 个有效仓库配置${NC}"
    
    # 显示找到的有效行(前5个)
    if [ $TOTAL_COUNT -gt 0 ] && [ $TOTAL_COUNT -le 10 ]; then
        echo -e "${CYAN}配置列表:${NC}"
        for i in "${!REPO_LINES[@]}"; do
            echo "  $((i+1)). ${REPO_LINES[$i]}"
        done
    elif [ $TOTAL_COUNT -gt 10 ]; then
        echo -e "${CYAN}配置列表(前5个):${NC}"
        for i in {0..4}; do
            [ $i -lt $TOTAL_COUNT ] && echo "  $((i+1)). ${REPO_LINES[$i]}"
        done
        echo "  ... 共 $TOTAL_COUNT 个配置"
    fi
    echo "================================"
fi

# 如果没有任何有效配置
if [ $TOTAL_COUNT -eq 0 ]; then
    echo -e "${RED}错误: 配置文件中没有找到有效的仓库配置${NC}" >&2
    exit 1
fi

# 同步单个仓库的函数
sync_repo() {
    local repo_dir=$1
    local target_branch=$2
    local current=$3
    local total=$4
    
    if [ "$QUIET" = false ]; then
        echo -e "\n${CYAN}[${current}/${total}]${NC} ${GREEN}处理:${NC} $repo_dir"
    else
        echo "[${current}/${total}] 处理: $repo_dir"
    fi
    
    # 检查目录是否存在
    if [ ! -d "$repo_dir" ]; then
        if [ "$QUIET" = false ]; then
            echo -e "  ${YELLOW}⚠ 跳过: 目录不存在${NC}"
        else
            echo "  跳过: 目录不存在"
        fi
        return 1
    fi
    
    # 检查是否是 git 仓库
    if [ ! -d "$repo_dir/.git" ]; then
        if [ "$QUIET" = false ]; then
            echo -e "  ${YELLOW}⚠ 跳过: 不是 git 仓库${NC}"
        else
            echo "  跳过: 不是 git 仓库"
        fi
        return 1
    fi
    
    # 进入目录
    cd "$repo_dir" 2>/dev/null || {
        echo "  错误: 无法进入目录 $repo_dir" >&2
        return 1
    }
    
    # 获取当前分支
    current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
    if [ -z "$current_branch" ]; then
        current_branch="HEAD (detached)"
    fi
    
    if [ "$QUIET" = false ]; then
        echo -e "  📍 当前分支: ${GREEN}$current_branch${NC}"
    fi
    
    # 如果指定了目标分支,尝试切换
    if [ -n "$target_branch" ]; then
        if [ "$QUIET" = false ]; then
            echo -e "  🎯 目标分支: ${GREEN}$target_branch${NC}"
        fi
        
        if [ "$current_branch" != "$target_branch" ] && [ "$current_branch" != "HEAD (detached)" ]; then
            if [ "$QUIET" = false ]; then
                echo -e "  🔄 切换到分支 $target_branch..."
            fi
            git checkout "$target_branch" 2>/dev/null || {
                if [ "$QUIET" = false ]; then
                    echo -e "  ${YELLOW}⚠ 警告: 无法切换到分支 $target_branch${NC}"
                fi
            }
        fi
    fi
    
    # 强制重置
    if [ "$FORCE_RESET" = true ]; then
        if [ "$QUIET" = false ]; then
            echo -e "  🔧 强制重置本地更改..."
        fi
        git reset --hard HEAD > /dev/null 2>&1
        git clean -fd > /dev/null 2>&1
    fi
    
    # 执行 git pull
    if [ "$QUIET" = false ]; then
        echo -e "  📥 执行 git pull $DEPTH..."
    fi
    
    if git pull $DEPTH > /tmp/git_pull_output 2>&1; then
        if [ "$QUIET" = false ]; then
            # 显示输出(但不要太啰嗦)
            if grep -q "Already up to date" /tmp/git_pull_output; then
                echo -e "  ${GREEN}✓ 已是最新${NC}"
            else
                cat /tmp/git_pull_output | sed 's/^/     /'
                echo -e "  ${GREEN}✓ 成功: $repo_dir 同步完成${NC}"
            fi
        fi
        
        # 显示最新提交(非静默模式)
        if [ "$QUIET" = false ]; then
            latest_commit=$(git log -1 --oneline 2>/dev/null)
            if [ -n "$latest_commit" ]; then
                echo -e "  📝 最新提交: $latest_commit"
            fi
        fi
        
        cd - > /dev/null 2>&1 || return 1
        rm -f /tmp/git_pull_output
        return 0
    else
        if [ "$QUIET" = false ]; then
            echo -e "  ${RED}✗ 失败: $repo_dir 同步失败${NC}"
            cat /tmp/git_pull_output | sed 's/^/     /'
        fi
        cd - > /dev/null 2>&1 || return 1
        rm -f /tmp/git_pull_output
        return 1
    fi
}

# 主程序
main() {
    local success_count=0
    local fail_count=0
    local skip_count=0
    local repo_index=0
    local start_time=$(date +%s)
    
    if [ "$QUIET" = false ]; then
        echo -e "${GREEN}开始同步Git仓库...${NC}"
        echo ""
    fi
    
    # 处理每个仓库
    for repo_line in "${REPO_LINES[@]}"; do
        repo_index=$((repo_index + 1))
        
        # 解析目录和可选的分支(跨平台兼容)
        local repo_dir=""
        local target_branch=""
        
        # 使用 printf + awk 避免 echo 的问题
        repo_dir=$(printf "%s" "$repo_line" | awk '{print $1}')
        target_branch=$(printf "%s" "$repo_line" | awk '{print $2}')
        
        # 如果 repo_dir 为空,跳过
        if [ -z "$repo_dir" ]; then
            if [ "$QUIET" = false ]; then
                echo -e "${YELLOW}[${repo_index}/${TOTAL_COUNT}] 跳过: 无效行 '$repo_line'${NC}"
            fi
            skip_count=$((skip_count + 1))
            continue
        fi
        
        # 去除末尾斜杠
        repo_dir=${repo_dir%/}
        
        if sync_repo "$repo_dir" "$target_branch" "$repo_index" "$TOTAL_COUNT"; then
            success_count=$((success_count + 1))
        else
            # 判断是失败还是跳过
            if [ ! -d "$repo_dir" ] || [ ! -d "$repo_dir/.git" ]; then
                skip_count=$((skip_count + 1))
            else
                fail_count=$((fail_count + 1))
            fi
        fi
        
        if [ "$QUIET" = false ] && [ $repo_index -lt $TOTAL_COUNT ]; then
            echo ""
        fi
    done
    
    # 计算耗时
    local end_time=$(date +%s)
    local elapsed=$((end_time - start_time))
    
    # 显示最终统计信息
    if [ "$QUIET" = false ]; then
        echo "================================"
        echo -e "${CYAN}📊 同步完成统计:${NC}"
        echo -e "  ${GREEN}✓ 成功: ${success_count}${NC}"
        echo -e "  ${RED}✗ 失败: ${fail_count}${NC}"
        echo -e "  ${YELLOW}⚠ 跳过: ${skip_count}${NC}"
        echo -e "  📦 总计: ${TOTAL_COUNT} 个仓库"
        echo -e "  ⏱️  耗时: ${elapsed} 秒"
    else
        echo "同步完成: 成功=${success_count} 失败=${fail_count} 跳过=${skip_count} 总计=${TOTAL_COUNT} 耗时=${elapsed}秒"
    fi
    
    if [ $fail_count -gt 0 ]; then
        return 1
    fi
    return 0
}

# 执行主程序
main
exit $?

二、使用说明

在脚本同目录下新建 pull_repos.conf 配置文件,一行一个 Git 仓库目录,然后运行:

bash
./pull_repos
点赞
收藏
暂无评论,快来发表评论吧~
私信
老牛,俗称哞哞。单纯的九零后理工小青年。喜欢折腾,爱玩,爱音乐,爱游戏,爱电影,爱旅游...
最后活跃于