电脑上项目比较多,经常会忘记拉取最新代码,导致时不时就会有冲突,因此就有了这个脚本,批量同步拉取,支持 Linux/macOS。
pull_repos
#!/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 仓库目录,然后运行:
./pull_repos