From b5552d7ffed6e613ddd8da1623d6d56c01ac5eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E5=98=89=E9=98=B3-coder?= Date: Sat, 22 Nov 2025 17:31:53 +0800 Subject: [PATCH] multi repo support --- .gitea/workflows/sync_A_to_B.yml | 58 ++++ .gitea/workflows/sync_B_to_C.yml | 58 ++++ README.md | 297 +++++++++++++----- configs/A_to_B.yaml | 22 ++ configs/B_to_C.yaml | 22 ++ scripts/__pycache__/sync_tool.cpython-311.pyc | Bin 0 -> 17627 bytes scripts/sync_tool.py | 293 +++++++++++++++++ 7 files changed, 673 insertions(+), 77 deletions(-) create mode 100644 .gitea/workflows/sync_A_to_B.yml create mode 100644 .gitea/workflows/sync_B_to_C.yml create mode 100644 configs/A_to_B.yaml create mode 100644 configs/B_to_C.yaml create mode 100644 scripts/__pycache__/sync_tool.cpython-311.pyc create mode 100644 scripts/sync_tool.py 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 0000000000000000000000000000000000000000..ed3338cca40a9aa112512bbc444f97b937cdc468 GIT binary patch literal 17627 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 R^F0Clo7WOl86@Mj{~zT>{=NVJ literal 0 HcmV?d00001 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()