diff --git a/.gitea/workflows/sync_A_to_B.yml b/.gitea/workflows/sync_A_to_B.yml new file mode 100644 index 0000000..018f08d --- /dev/null +++ b/.gitea/workflows/sync_A_to_B.yml @@ -0,0 +1,58 @@ +name: "同步 A → B" + +on: + push: + branches: + - master + paths: + - 'configs/A_to_B.yaml' + - 'scripts/sync_tool.py' + - '.gitea/workflows/sync_A_to_B.yml' + workflow_dispatch: + schedule: + # 每天凌晨2点执行 + - cron: '0 2 * * *' + +env: + CONFIG_FILE: 'configs/A_to_B.yaml' + +jobs: + sync-repositories: + runs-on: ubuntu-latest + + steps: + - name: 签出配置仓库 + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: 配置 Git + run: | + git config --global user.name "gitea-runner" + git config --global user.email "actions@gitea.fjy8018.top" + git config --global init.defaultBranch master + + - name: 安装依赖 + run: | + apt-get update + apt-get install -y python3-yaml + + - name: 验证配置文件 + run: | + if [ ! -f "${{ env.CONFIG_FILE }}" ]; then + echo "错误: 配置文件 ${{ env.CONFIG_FILE }} 不存在!" + exit 1 + fi + echo "✓ 配置文件已找到: ${{ env.CONFIG_FILE }}" + + - name: 执行同步 A → B + env: + SYNC_A_B_USERNAME: ${{ secrets.SYNC_A_B_USERNAME }} + SYNC_A_B_TOKEN: ${{ secrets.SYNC_A_B_TOKEN }} + run: | + echo "================================================================================" + echo "开始同步: A → B" + echo "使用配置: ${{ env.CONFIG_FILE }}" + echo "环境前缀: SYNC_A_B" + echo "================================================================================" + python3 scripts/sync_tool.py ${{ env.CONFIG_FILE }} --env-prefix SYNC_A_B diff --git a/.gitea/workflows/sync_B_to_C.yml b/.gitea/workflows/sync_B_to_C.yml new file mode 100644 index 0000000..69bf972 --- /dev/null +++ b/.gitea/workflows/sync_B_to_C.yml @@ -0,0 +1,58 @@ +name: "同步 B → C" + +on: + push: + branches: + - master + paths: + - 'configs/B_to_C.yaml' + - 'scripts/sync_tool.py' + - '.gitea/workflows/sync_B_to_C.yml' + workflow_dispatch: + schedule: + # 每天凌晨3点执行(在 A→B 之后) + - cron: '0 3 * * *' + +env: + CONFIG_FILE: 'configs/B_to_C.yaml' + +jobs: + sync-repositories: + runs-on: ubuntu-latest + + steps: + - name: 签出配置仓库 + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: 配置 Git + run: | + git config --global user.name "gitea-runner" + git config --global user.email "actions@gitea.fjy8018.top" + git config --global init.defaultBranch master + + - name: 安装依赖 + run: | + apt-get update + apt-get install -y python3-yaml + + - name: 验证配置文件 + run: | + if [ ! -f "${{ env.CONFIG_FILE }}" ]; then + echo "错误: 配置文件 ${{ env.CONFIG_FILE }} 不存在!" + exit 1 + fi + echo "✓ 配置文件已找到: ${{ env.CONFIG_FILE }}" + + - name: 执行同步 B → C + env: + SYNC_B_C_USERNAME: ${{ secrets.SYNC_B_C_USERNAME }} + SYNC_B_C_TOKEN: ${{ secrets.SYNC_B_C_TOKEN }} + run: | + echo "================================================================================" + echo "开始同步: B → C" + echo "使用配置: ${{ env.CONFIG_FILE }}" + echo "环境前缀: SYNC_B_C" + echo "================================================================================" + python3 scripts/sync_tool.py ${{ env.CONFIG_FILE }} --env-prefix SYNC_B_C diff --git a/README.md b/README.md index d328ced..dcaa917 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,151 @@ # 多仓库同步系统 -这是一个支持批量同步多个 Git 仓库的自动化流水线系统,适用于从上游仓库同步代码到目标 Gitea 仓库。 +这是一个支持多方向、多仓库批量同步的自动化流水线系统,适用于在不同 Git 服务器之间同步代码的场景。 ## 🚀 功能特性 -- **批量同步**:通过配置文件一次性管理多个仓库同步 +- **多方向同步**:支持 A→B, B→C, A→C 等多种同步方向 +- **独立配置**:每个同步方向使用单独的配置文件和流水线 +- **环境隔离**:每个方向使用不同的环境变量前缀,安全性更高 +- **批量同步**:每个方向可以通过配置文件管理多个仓库 +- **灵活调度**:每个方向可以配置不同的定时任务 +- **手动触发**:支持手动触发单个方向的同步 +- **详细日志**:提供每个仓库同步过程的详细日志 - **冲突自动处理**:使用 `reset --hard` 策略,确保目标仓库完全同步为上游状态 -- **定时执行**:支持定时任务(每天凌晨1点自动执行) -- **手动触发**:支持手动触发和配置文件更新时自动触发 -- **详细日志**:提供每个仓库同步过程的详细日志和总结报告 -- **安全可靠**:内置超时机制和错误处理 ## 📁 项目结构 ``` -. -├── repos.yaml # 仓库同步配置文件 +code-sync-project/ ├── .gitea/ │ └── workflows/ -│ └── multi-repo-sync.yml # 同步流水线定义 -└── README.md # 本文档 +│ ├── sync_A_to_B.yml # A→B 同步流水线 +│ └── sync_B_to_C.yml # B→C 同步流水线 +├── configs/ +│ ├── A_to_B.yaml # A→B 同步配置 +│ └── B_to_C.yaml # B→C 同步配置 +├── scripts/ +│ └── sync_tool.py # 核心同步工具(支持动态环境变量前缀) +├── repos.yaml # 旧版单配置文件(向后兼容) +├── sync_repos.py # 旧版同步脚本(向后兼容) +└── README.md # 本文档 ``` -**注意:** Gitea Actions 支持两种工作流路径: -- `.gitea/workflows/` (官方推荐) -- `.github/workflows/` (兼容性支持) - ## 🔧 配置说明 -### 1. 配置文件 (repos.yaml) +### 1. 配置 Secrets -在项目根目录的 `repos.yaml` 文件中定义需要同步的仓库: +在 Gitea 仓库的 **Settings > Secrets** 中配置以下变量: + +#### 对于 A→B 同步: +```bash +SYNC_A_B_USERNAME=your_username # 源A的用户名 +SYNC_A_B_TOKEN=your_token # 源A的访问令牌 +``` + +#### 对于 B→C 同步: +```bash +SYNC_B_C_USERNAME=your_username # 源B的用户名 +SYNC_B_C_TOKEN=your_token # 源B的访问令牌 +``` + +**注意**:每个同步方向使用不同的环境变量前缀,确保安全性。 + +### 2. 配置同步任务 + +编辑 `configs/A_to_B.yaml`: ```yaml -# 多仓库同步配置文件 -# 定义需要同步的仓库对,源仓库 -> 目标仓库 +name: "A_to_B_Sync" # 同步任务名称 + repositories: - # 示例:同步 BladeX-Tool 仓库 - - name: "bladex-tool" # 仓库名称(用于日志输出) - source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@center.javablade.com/blade/BladeX-Tool.git" - target_url: "https://${TARGET_USERNAME}:${TARGET_TOKEN}@gitea.fjy8018.top/home/BladeX-Tool.git" - branch: "master" # 要同步的分支 + - name: "bladex-tool" + source_url: "https://${USERNAME}:${TOKEN}@center.javablade.com/blade/BladeX-Tool.git" + target_url: "https://${USERNAME}:${TOKEN}@gitea.fjy8018.top/BladeX/BladeX-Tool.git" + branch: "master" - # 示例:同步另一个仓库(取消注释并修改以下配置) - - name: "another-repo" - source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@github.com/example/repo.git" - target_url: "https://${TARGET_USERNAME}:${TARGET_TOKEN}@gitea.fjy8018.top/home/repo.git" - branch: "main" + # 添加更多仓库... +``` - # 添加更多仓库同步配置... +编辑 `configs/B_to_C.yaml`: + +```yaml +name: "B_to_C_Sync" # 同步任务名称 + +repositories: + - name: "bladex-tool" + source_url: "https://${USERNAME}:${TOKEN}@gitea.fjy8018.top/BladeX/BladeX-Tool.git" + target_url: "https://${USERNAME}:${TOKEN}@backup.example.com/backup/BladeX-Tool.git" + branch: "master" + + # 添加更多仓库... ``` **配置字段说明:** | 字段 | 必填 | 说明 | |------|------|------| -| `name` | 是 | 仓库标识名称,用于日志输出 | -| `source_url` | 是 | 上游仓库地址,支持环境变量 | -| `target_url` | 是 | 目标 Gitea 仓库地址,支持环境变量 | -| `branch` | 否 | 要同步的分支,默认为 `master` | +| `name` | 是 | 同步任务名称,用于日志输出 | +| `repositories[].name` | 是 | 仓库标识名称 | +| `repositories[].source_url` | 是 | 上游仓库地址,使用 `${USERNAME}:${TOKEN}` 格式 | +| `repositories[].target_url` | 是 | 目标仓库地址,使用 `${USERNAME}:${TOKEN}` 格式 | +| `repositories[].branch` | 否 | 要同步的分支,默认为 `master` | -### 2. 环境变量 +### 3. 定时任务 -在 Gitea 仓库的 **Settings > Secrets** 中配置以下变量: +A→B:每天凌晨 2 点执行(`.gitea/workflows/sync_A_to_B.yml`) +```yaml +schedule: + - cron: '0 2 * * *' +``` -| 变量名 | 必填 | 说明 | -|--------|------|------| -| `UPSTREAM_USERNAME` | 是 | 上游仓库用户名 | -| `UPSTREAM_TOKEN` | 是 | 上游仓库访问令牌/密码 | -| `TARGET_USERNAME` | 是 | 目标 Gitea 用户名 | -| `TARGET_TOKEN` | 是 | 目标 Gitea 访问令牌 | +B→C:每天凌晨 3 点执行(`.gitea/workflows/sync_B_to_C.yml`) +```yaml +schedule: + - cron: '0 3 * * *' +``` -**注意:** 变量名不能以 `GITEA_` 或 `GITHUB_` 开头(这些是系统保留前缀),建议使用 `TARGET_` 或 `UPSTREAM_` 等前缀区分不同仓库的凭证。 - -**安全建议:** 使用 Personal Access Token (PAT) 而不是密码,以提高安全性。 +可以在对应的 workflow 文件中修改 cron 表达式。 ## 🚀 使用方法 -### 方法 1:手动触发 +### 方法 1:自动触发 -进入 Gitea 仓库页面的 **Actions** 标签页,选择 "多仓库同步流水线",点击 **Run workflow** 手动执行。 +当满足以下条件之一时自动触发: -### 方法 2:定时任务 +- 定时任务到达设定时间 +- 修改了对应的配置文件 +- 修改了对应的流水线文件 +- 修改了核心脚本 `sync_tool.py` -流水线默认配置为每天凌晨 1 点(UTC)自动执行。可在 `.gitea/workflows/multi-repo-sync.yml` 文件中修改 cron 表达式: +### 方法 2:手动触发 -```yaml -schedule: - - cron: '0 1 * * *' # 每天凌晨1点执行 -``` +进入 Gitea 仓库页面的 **Actions** 标签页: -### 方法 3:配置文件更新时自动触发 - -当修改 `repos.yaml` 或工作流文件时,会自动触发同步。 +1. 选择 "同步 A → B" 或 "同步 B → C" +2. 点击 **Run workflow** +3. 查看执行日志 ## 📊 执行结果 -执行成功后,可以在 Gitea Actions 页面查看: +执行后,Actions 页面会显示: -- 每个仓库的同步状态(成功/失败) -- 详细的同步日志 -- 同步完成总结报告 - -示例输出: ``` +================================================================================ +多仓库同步工具 +================================================================================ + +开始时间: 2025-11-22 10:00:00 +配置文件: configs/A_to_B.yaml +环境前缀: SYNC_A_B +✓ 配置任务: A_to_B_Sync +✓ 找到 2 个仓库配置 + +[1/2] 正在同步... ============================================================ 开始同步仓库: bladex-tool -源地址: center.javablade.com/blade/BladeX-Tool.git -目标地址: gitea.fjy8018.top/home/BladeX-Tool.git +源地址: ***:***@center.javablade.com/blade/BladeX-Tool.git +目标地址: ***:***@gitea.fjy8018.top/BladeX/BladeX-Tool.git 分支: master ============================================================ @@ -115,16 +153,103 @@ schedule: [2/6] 添加上游远程... [3/6] 获取上游更改... [4/6] 检查分支... -[5/6] 同步到上游分支... +[5/6] 同步到上游分支 (使用 reset --hard)... [6/6] 推送到目标仓库... ✅ [bladex-tool] 同步成功! + +================================================================================ +同步完成报告 +================================================================================ +✅ 成功 bladex-tool +✅ 成功 another-repo + +总计: 2 个仓库 +成功: 2 个 +失败: 0 个 + +✅ 所有仓库同步成功! + +结束时间: 2025-11-22 10:05:00 +``` + +## 🔧 添加新的同步方向 + +要添加新的同步方向(例如 A→C): + +### 步骤 1:创建配置文件 + +```bash +cp configs/A_to_B.yaml configs/A_to_C.yaml +``` + +编辑 `configs/A_to_C.yaml`: + +```yaml +name: "A_to_C_Sync" + +repositories: + - name: "bladex-tool" + source_url: "https://${USERNAME}:${TOKEN}@center.javablade.com/blade/BladeX-Tool.git" + target_url: "https://${USERNAME}:${TOKEN}@backup.example.com/backup/BladeX-Tool.git" + branch: "master" +``` + +### 步骤 2:创建流水线 + +```bash +cp .gitea/workflows/sync_A_to_B.yml .gitea/workflows/sync_A_to_C.yml +``` + +编辑 `.gitea/workflows/sync_A_to_C.yml`: + +```yaml +name: "同步 A → C" + +env: + CONFIG_FILE: 'configs/A_to_C.yaml' + +# ... 其他配置 ... + + - name: 执行同步 A → C + env: + SYNC_A_C_USERNAME: ${{ secrets.SYNC_A_C_USERNAME }} + SYNC_A_C_TOKEN: ${{ secrets.SYNC_A_C_TOKEN }} + run: | + python3 scripts/sync_tool.py ${{ env.CONFIG_FILE }} --env-prefix SYNC_A_C +``` + +### 步骤 3:配置 Secrets + +在 Gitea 中配置: +- `SYNC_A_C_USERNAME` +- `SYNC_A_C_TOKEN` + +### 步骤 4:提交并推送 + +```bash +git add configs/A_to_C.yaml .gitea/workflows/sync_A_to_C.yml +git commit -m "添加 A→C 同步方向" +git push +``` + +## 🛠️ 本地测试 + +在本地测试同步: + +```bash +# 设置环境变量 +export SYNC_A_B_USERNAME="your_username" +export SYNC_A_B_TOKEN="your_token" + +# 运行同步 +python3 scripts/sync_tool.py configs/A_to_B.yaml --env-prefix SYNC_A_B ``` ## ⚠️ 重要说明 -1. **强制同步策略**:本流水线使用 `git reset --hard` 策略,确保目标仓库**完全与上游一致**。任何在目标仓库上的修改都可能被覆盖。 +1. **强制同步策略**:使用 `git reset --hard` 策略,确保目标仓库**完全与上游一致**。任何在目标仓库上的修改都可能被覆盖。 -2. **权限要求**:确保提供的 Token 具有对目标仓库的写入权限。 +2. **权限要求**:提供的 Token 必须有对目标仓库的写入权限。 3. **冲突处理**:由于采用强制重置策略,不会保留目标仓库与上游的冲突修改。 @@ -144,32 +269,50 @@ schedule: ### 问题:仓库不存在 **解决方案**: -- 在目标 Gitea 上预先创建空仓库 +- 在目标服务器上预先创建空仓库 - 检查配置文件中的仓库地址是否正确 ### 问题:同步超时 **解决方案**: -- 在 `.gitea/workflows/multi-repo-sync.yml` 中增加超时时间 -- 对于大仓库,考虑使用浅克隆(已优化为 bare clone) +- 在 workflow 文件中增加超时时间 +- 检查网络连接是否稳定 -### 问题:配置文件解析错误 +### 问题:环境变量未设置 **解决方案**: -- 使用 YAML 格式验证工具检查 repos.yaml -- 确保所有必要字段都已填写 +- 检查是否在 Gitea Secrets 中配置了对应的变量 +- 检查环境变量前缀是否与配置文件中的一致 +- 查看日志中的环境变量检查输出 -## 📝 最佳实践 +## 📖 最佳实践 -1. **逐步添加仓库**:初次使用时,先配置 1-2 个仓库测试 -2. **使用变量**:将敏感信息和可变信息放在 Secrets 中 -3. **监控执行**:定期检查流水线执行日志 +1. **分环境配置**:不同环境(开发/测试/生产)使用不同的配置 +2. **逐步添加仓库**:初次使用时,先配置 1-2 个仓库测试 +3. **监控执行**:定期检查 Actions 执行日志 4. **测试环境**:先在测试仓库验证同步逻辑 5. **备份策略**:重要仓库确保有备份机制 +6. **安全隔离**:不同方向使用不同的 Token,降低风险 + +## 🔄 向后兼容 + +保留旧版文件以支持单配置文件场景: + +- `repos.yaml` - 单配置文件 +- `sync_repos.py` - 旧版同步脚本 +- `.gitea/workflows/multi-repo-sync.yml` - 旧版流水线 + +如需使用旧版,请配置以下 Secrets: +- `UPSTREAM_USERNAME` +- `UPSTREAM_TOKEN` +- `TARGET_USERNAME` +- `TARGET_TOKEN` + +**建议**:新项目使用新版多配置文件架构。 ## 🤝 贡献 -欢迎提交 Issue 和 Pull Request 来改进此项目。 +欢迎提交 Issue 和 Pull Request 来改进这个工具! ## 📄 许可证 diff --git a/configs/A_to_B.yaml b/configs/A_to_B.yaml new file mode 100644 index 0000000..fa098c4 --- /dev/null +++ b/configs/A_to_B.yaml @@ -0,0 +1,22 @@ +# A -> B 同步配置示例 +# 这个配置使用环境变量前缀: SYNC_A_B +# 需要在 Gitea Secrets 中配置: +# - SYNC_A_B_USERNAME +# - SYNC_A_B_TOKEN + +name: "A_to_B_Sync" # 同步任务名称,用于日志输出 + +repositories: + # 示例:从源A同步到目标B + - name: "bladex-tool" + source_url: "https://${USERNAME}:${TOKEN}@center.javablade.com/blade/BladeX-Tool.git" + target_url: "https://${USERNAME}:${TOKEN}@gitea.fjy8018.top/BladeX/BladeX-Tool.git" + branch: "master" + + # 示例:同步其他仓库(取消注释并修改) + # - name: "another-repo" + # source_url: "https://${USERNAME}:${TOKEN}@source.com/org/repo.git" + # target_url: "https://${USERNAME}:${TOKEN}@target.com/org/repo.git" + # branch: "main" + + # 添加更多仓库同步配置... diff --git a/configs/B_to_C.yaml b/configs/B_to_C.yaml new file mode 100644 index 0000000..9eb3e19 --- /dev/null +++ b/configs/B_to_C.yaml @@ -0,0 +1,22 @@ +# B -> C 同步配置示例 +# 这个配置使用环境变量前缀: SYNC_B_C +# 需要在 Gitea Secrets 中配置: +# - SYNC_B_C_USERNAME +# - SYNC_B_C_TOKEN + +name: "B_to_C_Sync" # 同步任务名称,用于日志输出 + +repositories: + # 示例:从源B同步到目标C + - name: "bladex-tool" + source_url: "https://${USERNAME}:${TOKEN}@gitea.fjy8018.top/BladeX/BladeX-Tool.git" + target_url: "https://${USERNAME}:${TOKEN}@target.com/backup/BladeX-Tool.git" + branch: "master" + + # 示例:同步其他仓库(取消注释并修改) + # - name: "another-repo" + # source_url: "https://${USERNAME}:${TOKEN}@source.com/org/repo.git" + # target_url: "https://${USERNAME}:${TOKEN}@target.com/org/repo.git" + # branch: "main" + + # 添加更多仓库同步配置... diff --git a/scripts/__pycache__/sync_tool.cpython-311.pyc b/scripts/__pycache__/sync_tool.cpython-311.pyc new file mode 100644 index 0000000..ed3338c Binary files /dev/null and b/scripts/__pycache__/sync_tool.cpython-311.pyc differ diff --git a/scripts/sync_tool.py b/scripts/sync_tool.py new file mode 100644 index 0000000..4db5151 --- /dev/null +++ b/scripts/sync_tool.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +多仓库同步工具 - 支持多方向同步 +支持动态环境变量前缀,适用于 A->B, B->C 等多场景 +""" +import yaml +import os +import sys +import subprocess +from pathlib import Path +import tempfile +import shutil +import time +import argparse + +def expand_env_vars(text, env_prefix): + """使用指定的前缀扩展环境变量""" + if not text: + return text + + # 构建完整的变量名 + var_mappings = { + 'USERNAME': f'{env_prefix}_USERNAME', + 'TOKEN': f'{env_prefix}_TOKEN', + } + + # 替换 ${VAR} 和 $VAR 格式 + for placeholder, env_var in var_mappings.items(): + value = os.environ.get(env_var, '') + if not value: + raise ValueError(f"环境变量 {env_var} 未设置") + + # 替换 ${VAR} + text = text.replace(f'${{{placeholder}}}', value) + # 替换 $VAR + text = text.replace(f'${placeholder}', value) + + # 如果还有未替换的变量,报错 + if '${' in text: + raise ValueError(f"URL 中包含未解析的变量: {text}") + + return text + +def sync_repository(repo_config, env_prefix): + """同步单个仓库""" + name = repo_config.get('name', 'unnamed-repo') + source_url_template = repo_config.get('source_url', '') + target_url_template = repo_config.get('target_url', '') + branch = repo_config.get('branch', 'master') + + if not source_url_template or not target_url_template: + print(f"❌ [{name}] 错误: 缺少 source_url 或 target_url") + return False + + try: + # 替换环境变量 + source_url = expand_env_vars(source_url_template, env_prefix) + target_url = expand_env_vars(target_url_template, env_prefix) + except ValueError as e: + print(f"❌ [{name}] 错误: {e}") + return False + + # 打印信息(隐藏敏感信息) + def hide_credentials(url): + if '@' in url: + parts = url.split('@') + return f"***:***@{parts[1]}" + return url + + print(f"\n{'='*60}") + print(f"开始同步仓库: {name}") + print(f"源地址: {hide_credentials(source_url)}") + print(f"目标地址: {hide_credentials(target_url)}") + print(f"分支: {branch}") + print(f"{'='*60}") + + # 创建临时工作目录 + work_dir = tempfile.mkdtemp(prefix=f"sync_{name}_") + try: + # 克隆目标仓库 + print(f"\n[1/6] 克隆目标仓库...") + result = subprocess.run( + ['git', 'clone', target_url, work_dir], + capture_output=True, text=True, timeout=300 + ) + if result.returncode != 0: + print(f"❌ 克隆目标仓库失败:") + print(result.stderr) + return False + + os.chdir(work_dir) + + # 添加上游远程 + print(f"[2/6] 添加上游远程...") + result = subprocess.run( + ['git', 'remote', 'add', 'upstream', source_url], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"❌ 添加上游远程失败:") + print(result.stderr) + return False + + # 获取上游更改 + print(f"[3/6] 获取上游更改...") + max_retries = 3 + for attempt in range(1, max_retries + 1): + try: + result = subprocess.run( + ['git', 'fetch', 'upstream'], + capture_output=True, text=True, timeout=200 + ) + if result.returncode == 0: + break + print(f"❌ 获取上游更改失败 (尝试 {attempt}/{max_retries}):") + print(result.stderr) + except subprocess.TimeoutExpired: + print(f"❌ 获取上游更改超时 (尝试 {attempt}/{max_retries}),200秒限制") + + if attempt < max_retries: + print(f"⏳ 等待5秒后重试...") + time.sleep(5) + else: + print(f"\n网络诊断信息:") + print(f"- 错误类型: DNS解析或网络连接失败") + print(f"- 可能原因:") + print(f" 1. DNS配置问题") + print(f" 2. 无法访问外部网络") + print(f" 3. 上游仓库服务器限制") + return False + + # 检查目标分支是否存在 + print(f"[4/6] 检查分支...") + result = subprocess.run( + ['git', 'branch', '-a'], + capture_output=True, text=True + ) + remote_branch = f'upstream/{branch}' + if remote_branch not in result.stdout: + print(f"❌ 上游分支 {branch} 不存在!") + print(f"可用的分支:") + print(result.stdout) + return False + + # 同步操作:重置到上游 + print(f"[5/6] 同步到上游分支 (使用 reset --hard)...") + subprocess.run(['git', 'fetch', 'origin'], capture_output=True) + result = subprocess.run( + ['git', 'reset', '--hard', f'upstream/{branch}'], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"❌ 同步失败:") + print(result.stderr) + return False + + # 推送到目标 + print(f"[6/6] 推送到目标仓库...") + result = subprocess.run( + ['git', 'push', '--force', '--tags', 'origin', f'refs/heads/{branch}'], + capture_output=True, text=True, timeout=600 + ) + if result.returncode != 0: + print(f"❌ 推送到目标仓库失败:") + print(result.stderr) + return False + + print(f"✅ [{name}] 同步成功!") + return True + + except subprocess.TimeoutExpired: + print(f"❌ [{name}] 操作超时!") + return False + except Exception as e: + print(f"❌ [{name}] 发生错误: {e}") + return False + finally: + # 返回原始目录并清理 + os.chdir('/') + if Path(work_dir).exists(): + shutil.rmtree(work_dir, ignore_errors=True) + +def load_config(config_file): + """加载配置文件""" + if not os.path.exists(config_file): + print(f"错误: 配置文件 {config_file} 不存在!") + sys.exit(1) + + try: + with open(config_file, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + if not isinstance(config, dict) or 'repositories' not in config: + print(f"错误: 配置文件格式不正确,需要包含 'repositories' 键") + sys.exit(1) + + repos = config['repositories'] + if not isinstance(repos, list) or len(repos) == 0: + print(f"警告: 配置文件中未找到仓库配置") + return [], None + + # 获取同步任务名称(可选) + sync_name = config.get('name', 'unnamed-sync') + + # 过滤掉注释掉的或空配置 + valid_repos = [ + repo for repo in repos + if isinstance(repo, dict) and + repo.get('name') and + not repo.get('name', '').strip().startswith('#') and + (repo.get('source_url') or '').strip() + ] + + print(f"✓ 配置任务: {sync_name}") + print(f"✓ 找到 {len(valid_repos)} 个仓库配置") + return valid_repos, sync_name + + except yaml.YAMLError as e: + print(f"错误: 解析YAML配置文件失败: {e}") + sys.exit(1) + except Exception as e: + print(f"错误: 读取配置文件失败: {e}") + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser(description='多仓库同步工具') + parser.add_argument('config_file', help='配置文件路径') + parser.add_argument('--env-prefix', required=True, help='环境变量前缀 (例如: SYNC_A_B)') + args = parser.parse_args() + + print("="*80) + print("多仓库同步工具") + print("="*80) + print(f"\n开始时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"配置文件: {args.config_file}") + print(f"环境前缀: {args.env_prefix}") + + # 加载配置 + repos, sync_name = load_config(args.config_file) + if not repos: + print("\n⚠️ 没有需要同步的仓库,退出") + sys.exit(0) + + # 检查必要的环境变量 + required_vars = [ + f'{args.env_prefix}_USERNAME', + f'{args.env_prefix}_TOKEN' + ] + missing_vars = [var for var in required_vars if not os.environ.get(var)] + if missing_vars: + print(f"\n❌ 错误: 缺少必要的环境变量: {', '.join(missing_vars)}") + print(f"请设置以下环境变量:") + print(f" - {args.env_prefix}_USERNAME") + print(f" - {args.env_prefix}_TOKEN") + sys.exit(1) + + print(f"✓ 所有必要环境变量已设置") + + # 同步所有仓库 + results = [] + for i, repo in enumerate(repos, 1): + print(f"\n[{i}/{len(repos)}] 正在同步...") + success = sync_repository(repo, args.env_prefix) + results.append((repo['name'], success)) + + # 在仓库之间等待一下,避免过于频繁的操作 + if i < len(repos): + time.sleep(5) + + # 总结报告 + print("\n" + "="*80) + print("同步完成报告") + print("="*80) + for name, success in results: + status = "✅ 成功" if success else "❌ 失败" + print(f"{status} {name}") + + successful = sum(1 for _, s in results if s) + failed = len(results) - successful + print(f"\n总计: {len(results)} 个仓库") + print(f"成功: {successful} 个") + print(f"失败: {failed} 个") + + if failed > 0: + print("\n❌ 有仓库同步失败,请查看详细日志") + sys.exit(1) + else: + print("\n✅ 所有仓库同步成功!") + + print(f"\n结束时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") + +if __name__ == '__main__': + main()