PR同步功能 #22
|
|
@ -0,0 +1,65 @@
|
|||
version: 2
|
||||
name: 未命名项目
|
||||
description: ""
|
||||
global:
|
||||
concurrent: 1
|
||||
trigger:
|
||||
webhook: gitlink@1.0.0
|
||||
event:
|
||||
- ref: push
|
||||
ruleset-operator: AND
|
||||
workflow:
|
||||
- ref: start
|
||||
name: 开始
|
||||
task: start
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
- ref: git_clone_0
|
||||
name: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
username: ((qqj.gitlink_user))
|
||||
password: ((qqj.gitlink_pwd))
|
||||
remote_url: '"https://gitlink.org.cn/lifedreamer/reposync.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: ssh执行命令
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_pass: ((qqj.ssh))
|
||||
ssh_ip: '"116.62.22.98"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_cmd: "\"docker stop group10 || true && docker rm group10 || true && docker
|
||||
pull
|
||||
crpi-tmvkwcuipqu5d1w2.cn-hongkong.personal.cr.aliyuncs.com/group_10/rep\
|
||||
o_sync:latest && docker run -d -p 8089:8000 --name group10 -e
|
||||
BOOT_MODE='app'
|
||||
crpi-tmvkwcuipqu5d1w2.cn-hongkong.personal.cr.aliyuncs.com/group_10/rep\
|
||||
o_sync:latest\""
|
||||
needs:
|
||||
- docker_image_build_0
|
||||
- ref: docker_image_build_0
|
||||
name: docker镜像构建
|
||||
task: docker_image_build@1.6.0
|
||||
input:
|
||||
docker_username: ((qqj.docker_user))
|
||||
docker_password: ((qqj.docker_pwd))
|
||||
image_name: '"crpi-tmvkwcuipqu5d1w2.cn-hongkong.personal.cr.aliyuncs.com/group_10/repo_sync"'
|
||||
image_tag: '"latest"'
|
||||
registry_address: '"crpi-tmvkwcuipqu5d1w2.cn-hongkong.personal.cr.aliyuncs.com"'
|
||||
docker_file: '"Dockerfile"'
|
||||
docker_build_path: '"."'
|
||||
workspace: git_clone_0.git_path
|
||||
image_push: true
|
||||
build_args: '""'
|
||||
needs:
|
||||
- git_clone_0
|
||||
|
||||
26
Dockerfile
26
Dockerfile
|
|
@ -1,27 +1,27 @@
|
|||
FROM centos:7
|
||||
|
||||
RUN yum update -y && \
|
||||
yum install -y wget gcc make openssl-devel bzip2-devel libffi-devel zlib-devel
|
||||
# 配置yum源为阿里云镜像
|
||||
RUN mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak && \
|
||||
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && \
|
||||
yum clean all && \
|
||||
yum makecache
|
||||
|
||||
RUN yum install -y wget gcc make openssl-devel bzip2-devel libffi-devel zlib-devel
|
||||
RUN wget -P /data/ob-tool https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz
|
||||
RUN cd /data/ob-tool && tar xzf Python-3.9.6.tgz
|
||||
RUN cd /data/ob-tool/Python-3.9.6 && ./configure --enable-optimizations && make altinstall
|
||||
|
||||
ADD ./ /data/ob-robot/
|
||||
RUN cd /data/ob-robot/ && \
|
||||
pip3.9 install -r /data/ob-robot/requirement.txt
|
||||
pip3.9 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r /data/ob-robot/requirement.txt
|
||||
|
||||
RUN yum install -y git openssh-server
|
||||
# Install OpenSSH and a modern version of Git from the IUS repository.
|
||||
# This avoids manual compilation, making the build faster and more reliable.
|
||||
RUN yum install -y openssh-server && \
|
||||
yum install -y https://repo.ius.io/ius-release-el7.rpm && \
|
||||
yum install -y git236
|
||||
|
||||
ENV GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa'
|
||||
RUN yum install -y autoconf gettext && \
|
||||
wget http://github.com/git/git/archive/v2.32.0.tar.gz && \
|
||||
tar -xvf v2.32.0.tar.gz && \
|
||||
rm -f v2.32.0.tar.gz && \
|
||||
cd git-* && \
|
||||
make configure && \
|
||||
./configure --prefix=/usr && \
|
||||
make -j16 && \
|
||||
make install
|
||||
|
||||
WORKDIR /data/ob-robot
|
||||
CMD if [ "$BOOT_MODE" = "app" ] ; then python3.9 main.py; fi
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import time
|
||||
import os
|
||||
import json
|
||||
|
||||
from fastapi import (
|
||||
BackgroundTasks,
|
||||
|
|
@ -23,6 +25,16 @@ from src.service.pull_request import PullRequestService
|
|||
from src.service.sync import ProjectService
|
||||
from src.utils import github
|
||||
|
||||
# 导入PR同步相关功能
|
||||
import requests
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# PR同步方向
|
||||
class SyncDirection(str, Enum):
|
||||
TO_GITEE = "to_gitee"
|
||||
TO_GITLINK = "to_gitlink"
|
||||
|
||||
|
||||
class PullRequest(Controller):
|
||||
|
||||
|
|
@ -170,6 +182,225 @@ class PullRequest(Controller):
|
|||
code=Code.SUCCESS,
|
||||
msg="第二期功能,敬请期待"
|
||||
)
|
||||
|
||||
@pull_request.post("/projects/{name}/cross-sync-pr", response_model=Response, description='跨平台同步PR')
|
||||
async def cross_sync_pr(
|
||||
self,
|
||||
name: str = Query(..., description='同步工程名称'),
|
||||
source_repo: str = Query(..., description='源仓库地址(格式:owner/repo)'),
|
||||
target_repo: str = Query(..., description='目标仓库地址(格式:owner/repo)'),
|
||||
direction: SyncDirection = Query(SyncDirection.TO_GITEE, description='同步方向,可选值:to_gitee(Gitlink到Gitee)或to_gitlink(Gitee到Gitlink)'),
|
||||
gitlink_cookie: Optional[str] = Query(None, description='Gitlink Cookie'),
|
||||
gitee_token: Optional[str] = Query(None, description='Gitee Token')
|
||||
):
|
||||
if not name or not source_repo or not target_repo:
|
||||
raise ErrorTemplate.ARGUMENT_LACK()
|
||||
|
||||
# 创建映射文件
|
||||
SYNC_MAPPING_FILE = f"pr_sync_mapping_{name}.json"
|
||||
REVERSE_SYNC_MAPPING_FILE = f"pr_sync_mapping_reverse_{name}.json"
|
||||
|
||||
# 尝试从环境变量或配置中获取令牌
|
||||
from src.base import config
|
||||
gitlink_cookie = gitlink_cookie or config.ACCOUNT.get('gitlink_cookie', '')
|
||||
gitee_token = gitee_token or config.ACCOUNT.get('gitee_token', '')
|
||||
|
||||
# 检查令牌是否有效
|
||||
if not gitlink_cookie:
|
||||
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitlink Cookie 不能为空")
|
||||
if not gitee_token:
|
||||
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitee Token 不能为空")
|
||||
|
||||
# 使用后台任务执行同步,避免阻塞API请求
|
||||
try:
|
||||
# 导入PR同步功能
|
||||
from src.common.pr_sync import (
|
||||
load_sync_mapping, save_sync_mapping, get_gitlink_pulls, get_gitee_branches,
|
||||
create_gitee_pull, test_gitee_api, get_gitlink_pull_comments,
|
||||
create_gitee_pull_comment, sync_pull_comments, get_gitlink_pr_index,
|
||||
check_can_sync_pull_request, get_gitee_pulls, create_gitlink_pull,
|
||||
check_can_sync_to_gitlink, sync_gitee_to_gitlink, sync_gitlink_to_gitee,
|
||||
init_config
|
||||
)
|
||||
|
||||
# 初始化配置
|
||||
init_config(source_repo, target_repo, gitlink_cookie, gitee_token)
|
||||
|
||||
# 根据同步方向执行不同的同步逻辑
|
||||
if direction == SyncDirection.TO_GITEE:
|
||||
# 从Gitlink同步PR到Gitee
|
||||
# 这里直接调用模块中的函数,传入必要的参数
|
||||
results = await self._run_sync_gitlink_to_gitee(
|
||||
source_repo=source_repo,
|
||||
target_repo=target_repo,
|
||||
gitlink_cookie=gitlink_cookie,
|
||||
gitee_token=gitee_token,
|
||||
sync_mapping_file=SYNC_MAPPING_FILE
|
||||
)
|
||||
return Response(
|
||||
code=Code.SUCCESS,
|
||||
data=results,
|
||||
msg=f"从Gitlink同步PR到Gitee成功,共同步{results.get('success_count', 0)}个PR"
|
||||
)
|
||||
else:
|
||||
# 从Gitee同步PR到Gitlink
|
||||
# 这里直接调用模块中的函数,传入必要的参数
|
||||
results = await self._run_sync_gitee_to_gitlink(
|
||||
source_repo=target_repo, # 注意:这里反过来了
|
||||
target_repo=source_repo, # 注意:这里反过来了
|
||||
gitlink_cookie=gitlink_cookie,
|
||||
gitee_token=gitee_token,
|
||||
sync_mapping_file=REVERSE_SYNC_MAPPING_FILE
|
||||
)
|
||||
return Response(
|
||||
code=Code.SUCCESS,
|
||||
data=results,
|
||||
msg=f"从Gitee同步PR到Gitlink成功,共同步{results.get('success_count', 0)}个PR"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"跨平台同步PR失败: {str(e)}")
|
||||
raise ErrorTemplate.TIP_ARGUMENT_ERROR(f"同步PR失败: {str(e)}")
|
||||
|
||||
async def _run_sync_gitlink_to_gitee(self, source_repo, target_repo, gitlink_cookie, gitee_token, sync_mapping_file):
|
||||
"""从Gitlink同步PR到Gitee的具体实现"""
|
||||
# 导入必要的函数
|
||||
from src.common.pr_sync import (
|
||||
load_sync_mapping, save_sync_mapping, get_gitlink_pulls, get_gitee_branches,
|
||||
create_gitee_pull, test_gitee_api, get_gitlink_pull_comments,
|
||||
create_gitee_pull_comment, sync_pull_comments, get_gitlink_pr_index,
|
||||
check_can_sync_pull_request, init_config
|
||||
)
|
||||
|
||||
# 初始化配置
|
||||
init_config(source_repo, target_repo, gitlink_cookie, gitee_token)
|
||||
|
||||
# 加载同步映射
|
||||
sync_mapping = load_sync_mapping(sync_mapping_file)
|
||||
|
||||
# 测试Gitee API
|
||||
if not test_gitee_api():
|
||||
return {"success": False, "message": "API测试失败,请检查配置"}
|
||||
|
||||
# 获取Gitlink的PR列表
|
||||
pulls = get_gitlink_pulls()
|
||||
if not pulls:
|
||||
return {"success": True, "message": "没有获取到Gitlink的PR列表", "success_count": 0}
|
||||
|
||||
# 筛选可同步的PR
|
||||
syncable_pulls = []
|
||||
for pull in pulls:
|
||||
gitlink_pr_id = pull.get("id")
|
||||
|
||||
# 检查是否已同步
|
||||
if str(gitlink_pr_id) in sync_mapping:
|
||||
continue
|
||||
|
||||
# 检查是否可同步
|
||||
can_sync, reason = check_can_sync_pull_request(pull)
|
||||
if can_sync:
|
||||
syncable_pulls.append(pull)
|
||||
|
||||
if not syncable_pulls:
|
||||
return {"success": True, "message": "没有发现可同步的PR", "success_count": 0}
|
||||
|
||||
# 同步每个PR
|
||||
success_count = 0
|
||||
for pull in syncable_pulls:
|
||||
gitlink_pr_id = pull.get("id")
|
||||
|
||||
# 在Gitee创建PR
|
||||
gitee_pr = create_gitee_pull(pull)
|
||||
if not gitee_pr:
|
||||
continue
|
||||
|
||||
gitee_pr_id = gitee_pr.get("id") or gitee_pr.get("number")
|
||||
success_count += 1
|
||||
|
||||
# 保存同步映射
|
||||
sync_mapping[str(gitlink_pr_id)] = gitee_pr_id
|
||||
|
||||
# 同步评论
|
||||
sync_pull_comments(gitlink_pr_id, gitee_pr_id)
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(1)
|
||||
|
||||
# 保存同步映射
|
||||
save_sync_mapping(sync_mapping, sync_mapping_file)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"同步完成!成功: {success_count}/{len(syncable_pulls)} 个PR",
|
||||
"success_count": success_count,
|
||||
"total_count": len(syncable_pulls)
|
||||
}
|
||||
|
||||
async def _run_sync_gitee_to_gitlink(self, source_repo, target_repo, gitlink_cookie, gitee_token, sync_mapping_file):
|
||||
"""从Gitee同步PR到Gitlink的具体实现"""
|
||||
# 导入必要的函数
|
||||
from src.common.pr_sync import (
|
||||
load_sync_mapping, save_sync_mapping, get_gitee_pulls,
|
||||
create_gitlink_pull, check_can_sync_to_gitlink, init_config
|
||||
)
|
||||
|
||||
# 初始化配置
|
||||
init_config(source_repo, target_repo, gitlink_cookie, gitee_token)
|
||||
|
||||
# 加载同步映射
|
||||
sync_mapping = load_sync_mapping(sync_mapping_file)
|
||||
|
||||
# 获取Gitee的PR列表
|
||||
pulls = get_gitee_pulls()
|
||||
if not pulls:
|
||||
return {"success": True, "message": "没有获取到Gitee的PR列表", "success_count": 0}
|
||||
|
||||
# 筛选可同步的PR
|
||||
syncable_pulls = []
|
||||
for pull in pulls:
|
||||
gitee_pr_id = pull.get("id")
|
||||
|
||||
# 检查是否已同步
|
||||
if str(gitee_pr_id) in sync_mapping:
|
||||
continue
|
||||
|
||||
# 检查是否可同步
|
||||
can_sync, reason = check_can_sync_to_gitlink(pull)
|
||||
if can_sync:
|
||||
syncable_pulls.append(pull)
|
||||
|
||||
if not syncable_pulls:
|
||||
return {"success": True, "message": "没有发现可同步的PR", "success_count": 0}
|
||||
|
||||
# 同步每个PR
|
||||
success_count = 0
|
||||
for pull in syncable_pulls:
|
||||
gitee_pr_id = pull.get("id")
|
||||
gitee_pr_number = pull.get("number")
|
||||
|
||||
# 在Gitlink创建PR
|
||||
gitlink_pr = create_gitlink_pull(pull)
|
||||
if not gitlink_pr:
|
||||
continue
|
||||
|
||||
gitlink_pr_id = gitlink_pr.get("pull_request_id")
|
||||
success_count += 1
|
||||
|
||||
# 保存同步映射
|
||||
sync_mapping[str(gitee_pr_id)] = gitlink_pr_id
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(1)
|
||||
|
||||
# 保存同步映射
|
||||
save_sync_mapping(sync_mapping, sync_mapping_file)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"反向同步完成!成功: {success_count}/{len(syncable_pulls)} 个PR",
|
||||
"success_count": success_count,
|
||||
"total_count": len(syncable_pulls)
|
||||
}
|
||||
|
||||
async def _check_project(self, name: str):
|
||||
project_service = ProjectService()
|
||||
|
|
|
|||
|
|
@ -33,18 +33,18 @@ buc_key and ConfigsUtil.set_obfastapi_config('buc_key', buc_key)
|
|||
DB_ENV = getenv('DB_ENV', 'test_env')
|
||||
DB = {
|
||||
'test_env': {
|
||||
'host': getenv('CEROBOT_MYSQL_HOST', ''),
|
||||
'port': getenv('CEROBOT_MYSQL_PORT', 2883, int),
|
||||
'user': getenv('CEROBOT_MYSQL_USER', ''),
|
||||
'passwd': getenv('CEROBOT_MYSQL_PWD', ''),
|
||||
'dbname': getenv('CEROBOT_MYSQL_DB', '')
|
||||
'host': getenv('CEROBOT_MYSQL_HOST', 'localhost'),
|
||||
'port': getenv('CEROBOT_MYSQL_PORT', 3306, int),
|
||||
'user': getenv('CEROBOT_MYSQL_USER', 'root'),
|
||||
'passwd': getenv('CEROBOT_MYSQL_PWD', 'Qian15825254201'),
|
||||
'dbname': getenv('CEROBOT_MYSQL_DB', 'repo_sync')
|
||||
},
|
||||
'local': {
|
||||
'host': getenv('CEROBOT_MYSQL_HOST', ''),
|
||||
'port': getenv('CEROBOT_MYSQL_PORT', 2881, int),
|
||||
'user': getenv('CEROBOT_MYSQL_USER', ''),
|
||||
'passwd': getenv('CEROBOT_MYSQL_PWD', ''),
|
||||
'dbname': getenv('CEROBOT_MYSQL_DB', '')
|
||||
'host': getenv('CEROBOT_MYSQL_HOST', 'localhost'),
|
||||
'port': getenv('CEROBOT_MYSQL_PORT', 3306, int),
|
||||
'user': getenv('CEROBOT_MYSQL_USER', 'root'),
|
||||
'passwd': getenv('CEROBOT_MYSQL_PWD', 'Qian15825254201'),
|
||||
'dbname': getenv('CEROBOT_MYSQL_DB', 'repo_sync')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,12 +60,13 @@ ACCOUNT = {
|
|||
'username': getenv('OB_ROBOT_USERNAME', ''),
|
||||
'email': getenv('OB_ROBOT_USERNAME', ''),
|
||||
'github_token': getenv('GITHUB_TOKEN', ''),
|
||||
'gitee_token': getenv('GITEE_TOKEN', ''),
|
||||
'gitee_token': getenv('GITEE_TOKEN', '821e3b39e558457d50486124315f8782'),
|
||||
'gitlab_token': getenv('GITLAB_TOKEN', ''), # 暂时还是我的token,待替代为一个内部账号
|
||||
'antcode_token': getenv('ANTCODE_TOKEN', ''), # 暂时还是我的token,待替代为一个内部账号
|
||||
'gitcode_token': getenv('GITCODE_TOKEN', ''), # 暂时还是我的token,待替代为ob-robot账号
|
||||
'robot_code_token': getenv('ROBOT_CODE_TOKEN', ''),
|
||||
'robot_antcode_token': getenv('ROBOT_ANTCODE_TOKEN', '')
|
||||
'robot_antcode_token': getenv('ROBOT_ANTCODE_TOKEN', ''),
|
||||
'gitlink_cookie': getenv('GITLINK_COOKIE', 'a16c742d837d868dc504cf3a7d37e06651eeef7d')
|
||||
}
|
||||
|
||||
GITLAB_ENV = {
|
||||
|
|
@ -84,8 +85,8 @@ GITEE_ENV = {
|
|||
}
|
||||
|
||||
GITLINK_ENV = {
|
||||
'gitlink_api_address': getenv('GITLINK_API_HOST', ''),
|
||||
'gitlink_api_diff_address': getenv('GITLINK_API_DIFF_HOST', ''),
|
||||
'gitlink_api_address': getenv('GITLINK_API_HOST', 'https://www.gitlink.org.cn/api'),
|
||||
'gitlink_api_diff_address': getenv('GITLINK_API_DIFF_HOST', 'https://www.gitlink.org.cn'),
|
||||
}
|
||||
|
||||
GITCODE_ENV = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,714 @@
|
|||
import requests
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
|
||||
# 配置信息
|
||||
GITLINK_COOKIE = None # 将在运行时设置
|
||||
GITEE_TOKEN = None # 将在运行时设置
|
||||
SOURCE_REPO = None # 将在运行时设置
|
||||
TARGET_REPO = None # 将在运行时设置
|
||||
|
||||
# 创建同步映射文件保存同步记录
|
||||
SYNC_MAPPING_FILE = "pr_sync_mapping.json"
|
||||
REVERSE_SYNC_MAPPING_FILE = "pr_sync_mapping_reverse.json"
|
||||
|
||||
def init_config(source_repo, target_repo, gitlink_cookie, gitee_token):
|
||||
"""初始化配置信息"""
|
||||
global SOURCE_REPO, TARGET_REPO, GITLINK_COOKIE, GITEE_TOKEN
|
||||
SOURCE_REPO = source_repo
|
||||
TARGET_REPO = target_repo
|
||||
GITLINK_COOKIE = gitlink_cookie
|
||||
GITEE_TOKEN = gitee_token
|
||||
|
||||
def load_sync_mapping(file_name):
|
||||
"""加载已同步的PR映射关系"""
|
||||
if os.path.exists(file_name):
|
||||
with open(file_name, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_sync_mapping(mapping, file_name):
|
||||
"""保存同步映射关系"""
|
||||
with open(file_name, 'w') as f:
|
||||
json.dump(mapping, f, indent=2)
|
||||
|
||||
def get_gitlink_pulls():
|
||||
"""从Gitlink获取PR列表"""
|
||||
global SOURCE_REPO, GITLINK_COOKIE
|
||||
url = f"https://www.gitlink.org.cn/api/v1/{SOURCE_REPO}/pulls.json"
|
||||
headers = {
|
||||
"Cookie": f"autologin_trustie={GITLINK_COOKIE}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()["pulls"]
|
||||
except Exception as e:
|
||||
print(f"获取Gitlink PR失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_gitee_branches():
|
||||
"""获取Gitee仓库的分支列表"""
|
||||
global TARGET_REPO, GITEE_TOKEN
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/branches"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
branches = response.json()
|
||||
return [branch["name"] for branch in branches]
|
||||
except Exception as e:
|
||||
print(f"获取Gitee分支列表失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def create_gitee_pull(pull_data):
|
||||
"""在Gitee创建PR"""
|
||||
global TARGET_REPO, GITEE_TOKEN
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/pulls"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 处理分支名称 - 去掉可能的前缀
|
||||
head_branch = pull_data.get("head", "").replace("refs/heads/", "").replace("origin/", "")
|
||||
base_branch = pull_data.get("base", "").replace("refs/heads/", "").replace("origin/", "")
|
||||
|
||||
# 构建PR数据
|
||||
payload = {
|
||||
"title": pull_data.get("title", "未命名PR"),
|
||||
"head": head_branch,
|
||||
"base": base_branch,
|
||||
"body": f"从Gitlink同步的PR\n\n原始PR信息:\n- 标题: {pull_data.get('title', 'N/A')}\n- 原始链接: https://gitlink.org.cn/{SOURCE_REPO}/pulls/{pull_data.get('id', 'N/A')}\n- 状态: {pull_data.get('state', 'N/A')}\n\n{pull_data.get('body', '')}"
|
||||
}
|
||||
|
||||
# 打印创建信息
|
||||
print(f" - 标题: {payload['title']}")
|
||||
print(f" - 源分支: {payload['head']}")
|
||||
print(f" - 目标分支: {payload['base']}")
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
# 处理响应
|
||||
if response.status_code != 201:
|
||||
error_message = "未知错误"
|
||||
try:
|
||||
error_data = response.json()
|
||||
if "message" in error_data:
|
||||
error_message = error_data['message']
|
||||
except:
|
||||
error_message = f"状态码: {response.status_code}"
|
||||
|
||||
print(f" ✗ 创建失败: {error_message}")
|
||||
return None
|
||||
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f" ✗ 请求异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def test_gitee_api():
|
||||
"""测试Gitee API连接和权限"""
|
||||
global TARGET_REPO, GITEE_TOKEN
|
||||
print("测试Gitee API连接...")
|
||||
|
||||
# 测试基本连接
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
repo_info = response.json()
|
||||
print(f"✓ 成功连接到仓库: {repo_info.get('full_name')}")
|
||||
print(f" - 默认分支: {repo_info.get('default_branch')}")
|
||||
print(f" - 权限: {repo_info.get('permissions', {})}")
|
||||
else:
|
||||
print(f"✗ 无法访问仓库,状态码: {response.status_code}")
|
||||
print(f"响应: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ API连接失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 测试分支列表
|
||||
branches = get_gitee_branches()
|
||||
if branches:
|
||||
print(f"✓ 获取到 {len(branches)} 个分支: {branches}")
|
||||
else:
|
||||
print("✗ 无法获取分支列表")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_gitlink_pull_comments(pull_index):
|
||||
"""从Gitlink获取PR评论列表"""
|
||||
global SOURCE_REPO, GITLINK_COOKIE
|
||||
# 将SOURCE_REPO拆分为owner和repo
|
||||
repo_parts = SOURCE_REPO.split('/')
|
||||
if len(repo_parts) != 2:
|
||||
print(f" ✗ 仓库格式错误: {SOURCE_REPO},应为'owner/repo'格式")
|
||||
return []
|
||||
|
||||
owner, repo = repo_parts
|
||||
|
||||
url = f"https://www.gitlink.org.cn/api/v1/{owner}/{repo}/pulls/{pull_index}/journals.json"
|
||||
|
||||
# 确保cookie格式正确
|
||||
cookies = {
|
||||
"autologin_trustie": GITLINK_COOKIE
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
}
|
||||
|
||||
params = {
|
||||
"is_full": "true", # 获取全部评论包括回复
|
||||
"sort_by": "created_on", # 根据API文档,正确的值是created_on,而不是created_at
|
||||
"sort_direction": "asc" # 按创建时间正序排列
|
||||
}
|
||||
|
||||
try:
|
||||
# 使用cookies参数而不是在headers中设置Cookie
|
||||
response = requests.get(url, headers=headers, cookies=cookies, params=params)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f" ✗ 获取评论失败,状态码: {response.status_code}")
|
||||
return []
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
|
||||
# 如果返回错误信息
|
||||
if data.get("status", 0) < 0:
|
||||
print(f" ✗ API错误: {data.get('message', '未知错误')}")
|
||||
return []
|
||||
|
||||
# 打印找到的评论数量
|
||||
comments_count = len(data.get("journals", []))
|
||||
return data.get("journals", [])
|
||||
except json.JSONDecodeError:
|
||||
print(f" ✗ 响应内容不是有效的JSON")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 获取评论异常: {str(e)}")
|
||||
return []
|
||||
|
||||
def create_gitee_pull_comment(gitee_pr_number, comment_data):
|
||||
"""在Gitee创建PR评论"""
|
||||
global TARGET_REPO, GITEE_TOKEN
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/pulls/{gitee_pr_number}/comments"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 构建评论内容,包含原始作者信息
|
||||
original_author = comment_data.get("user", {}).get("name", "Unknown")
|
||||
original_login = comment_data.get("user", {}).get("login", "unknown")
|
||||
original_time = comment_data.get("created_at", "")
|
||||
original_note = comment_data.get("note", "")
|
||||
|
||||
# 构建评论正文,标注原始作者
|
||||
comment_body = f"**原始评论作者**: {original_author} (@{original_login})\n"
|
||||
comment_body += f"**原始评论时间**: {original_time}\n\n"
|
||||
comment_body += f"{original_note}"
|
||||
|
||||
# 如果是代码评论,添加路径和行号信息
|
||||
if comment_data.get("path") and comment_data.get("line_code"):
|
||||
comment_body += f"\n\n*原始位置*: `{comment_data.get('path')}` (行: {comment_data.get('line_code')})"
|
||||
|
||||
payload = {
|
||||
"body": comment_body
|
||||
}
|
||||
|
||||
# 如果是代码评论,尝试添加相关信息(注意:Gitee的代码评论需要更多参数)
|
||||
if comment_data.get("commit_id"):
|
||||
payload["commit_id"] = comment_data["commit_id"]
|
||||
if comment_data.get("path"):
|
||||
payload["path"] = comment_data["path"]
|
||||
if comment_data.get("line_code"):
|
||||
# 尝试从line_code中提取position信息
|
||||
try:
|
||||
# line_code格式通常是: commit_id_old_line_new_line
|
||||
parts = comment_data["line_code"].split("_")
|
||||
if len(parts) >= 3:
|
||||
payload["position"] = int(parts[-1]) # 使用最后一部分作为position
|
||||
except:
|
||||
pass # 如果解析失败,就作为普通评论处理
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"创建评论失败,状态码: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
# 如果代码评论创建失败,尝试创建普通评论
|
||||
if "commit_id" in payload or "path" in payload or "position" in payload:
|
||||
print("尝试创建普通评论...")
|
||||
simple_payload = {"body": comment_body}
|
||||
response = requests.post(url, headers=headers, json=simple_payload)
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建Gitee评论失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def sync_pull_comments(gitlink_pr_id, gitee_pr_number):
|
||||
"""同步PR评论"""
|
||||
print(f"开始同步PR评论...")
|
||||
|
||||
# 需要查找PR的索引(index)而不是ID
|
||||
gitlink_pr_index = get_gitlink_pr_index(gitlink_pr_id)
|
||||
if gitlink_pr_index is None:
|
||||
print(f" ✗ 无法获取PR索引,无法同步评论")
|
||||
return
|
||||
|
||||
comments = get_gitlink_pull_comments(gitlink_pr_index)
|
||||
if not comments:
|
||||
print(" - 没有找到评论,无需同步")
|
||||
return
|
||||
|
||||
print(f" - 找到 {len(comments)} 条评论,开始同步")
|
||||
|
||||
# 按parent_id分组处理评论(先处理顶级评论,再处理回复)
|
||||
top_level_comments = [c for c in comments if not c.get("parent_id")]
|
||||
reply_comments = [c for c in comments if c.get("parent_id")]
|
||||
|
||||
successful_syncs = 0
|
||||
|
||||
# 先同步顶级评论
|
||||
if top_level_comments:
|
||||
print(f" - 同步 {len(top_level_comments)} 条顶级评论")
|
||||
for comment in top_level_comments:
|
||||
comment_id = comment.get("id")
|
||||
author = comment.get("user", {}).get("name", "Unknown")
|
||||
|
||||
result = create_gitee_pull_comment(gitee_pr_number, comment)
|
||||
if result:
|
||||
successful_syncs += 1
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(0.5)
|
||||
|
||||
# 再同步回复评论
|
||||
if reply_comments:
|
||||
print(f" - 同步 {len(reply_comments)} 条回复评论")
|
||||
for comment in reply_comments:
|
||||
comment_id = comment.get("id")
|
||||
parent_id = comment.get("parent_id")
|
||||
|
||||
# 为回复评论添加额外的上下文信息
|
||||
original_comment = comment.copy()
|
||||
original_comment["note"] = f"[回复评论#{parent_id}]\n\n{comment.get('note', '')}"
|
||||
|
||||
result = create_gitee_pull_comment(gitee_pr_number, original_comment)
|
||||
if result:
|
||||
successful_syncs += 1
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(0.5)
|
||||
|
||||
print(f" ✓ 评论同步完成: {successful_syncs}/{len(comments)} 条成功")
|
||||
|
||||
def get_gitlink_pr_index(pr_id):
|
||||
"""根据PR ID获取PR索引(index)"""
|
||||
# 获取所有PR列表
|
||||
pulls = get_gitlink_pulls()
|
||||
|
||||
# 查找匹配ID的PR
|
||||
for pull in pulls:
|
||||
pull_id = pull.get("id")
|
||||
pull_index = pull.get("index")
|
||||
|
||||
if pull_id == pr_id:
|
||||
return pull_index
|
||||
|
||||
return None
|
||||
|
||||
def check_can_sync_pull_request(pull_data):
|
||||
"""检查PR是否可以同步,返回(可同步, 原因)"""
|
||||
# 获取Gitee仓库的分支列表
|
||||
available_branches = get_gitee_branches()
|
||||
|
||||
# 处理分支名称 - 去掉可能的前缀
|
||||
head_branch = pull_data.get("head", "").replace("refs/heads/", "").replace("origin/", "")
|
||||
base_branch = pull_data.get("base", "").replace("refs/heads/", "").replace("origin/", "")
|
||||
|
||||
# 如果分支名称为空,使用默认值
|
||||
if not head_branch:
|
||||
return False, "源分支名称为空"
|
||||
if not base_branch:
|
||||
return False, "目标分支名称为空"
|
||||
|
||||
# 检查分支是否存在
|
||||
if head_branch not in available_branches:
|
||||
return False, f"源分支 '{head_branch}' 在目标仓库中不存在"
|
||||
|
||||
if base_branch not in available_branches:
|
||||
return False, f"目标分支 '{base_branch}' 在目标仓库中不存在"
|
||||
|
||||
# 检查源分支和目标分支是否有差异
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/compare/{base_branch}...{head_branch}"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
data = response.json()
|
||||
|
||||
# 如果没有提交差异,则不需要同步
|
||||
if not data.get("commits") or len(data.get("commits", [])) == 0:
|
||||
return False, f"源分支和目标分支没有差异"
|
||||
except Exception as e:
|
||||
# 如果无法检查差异,默认认为可以同步
|
||||
pass
|
||||
|
||||
# 检查是否已存在相同分支的PR
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/pulls"
|
||||
params = {
|
||||
"state": "open", # 只检查开放的PR
|
||||
"head": head_branch,
|
||||
"base": base_branch
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
pulls = response.json()
|
||||
|
||||
if pulls and len(pulls) > 0:
|
||||
pr_numbers = [f"#{pr['number']} {pr['title']}" for pr in pulls]
|
||||
return False, f"已存在相同源分支、目标分支的PR: {', '.join(pr_numbers)}"
|
||||
except Exception as e:
|
||||
# 如果无法检查现有PR,默认认为可以同步
|
||||
pass
|
||||
|
||||
return True, "可以同步"
|
||||
|
||||
# ================== 反向同步功能 (Gitee -> Gitlink) ==================
|
||||
|
||||
def get_gitee_pulls():
|
||||
"""从Gitee获取PR列表"""
|
||||
global TARGET_REPO, GITEE_TOKEN
|
||||
url = f"https://gitee.com/api/v5/repos/{TARGET_REPO}/pulls"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEE_TOKEN}"
|
||||
}
|
||||
params = {
|
||||
"state": "open", # 只获取开放状态的PR
|
||||
"sort": "created",
|
||||
"direction": "desc",
|
||||
"per_page": 100
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"获取Gitee PR失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def create_gitlink_pull(pull_data):
|
||||
"""在Gitlink创建PR"""
|
||||
global SOURCE_REPO, GITLINK_COOKIE
|
||||
owner, repo = SOURCE_REPO.split('/')
|
||||
url = f"https://www.gitlink.org.cn/api/{owner}/{repo}/pulls.json"
|
||||
|
||||
headers = {
|
||||
"Cookie": f"autologin_trustie={GITLINK_COOKIE}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 处理分支名称
|
||||
head_branch = pull_data.get("head", {}).get("ref", "").replace("refs/heads/", "")
|
||||
base_branch = pull_data.get("base", {}).get("ref", "").replace("refs/heads/", "")
|
||||
|
||||
# 尝试不同的payload配置
|
||||
payloads_to_try = [
|
||||
# 配置1:按照API示例,非fork仓库
|
||||
{
|
||||
"title": pull_data.get("title", "未命名PR"),
|
||||
"body": f"从Gitee同步的PR\n\n原始PR信息:\n- 标题: {pull_data.get('title', 'N/A')}\n- 原始链接: {pull_data.get('html_url', 'N/A')}\n- 状态: {pull_data.get('state', 'N/A')}\n\n{pull_data.get('body', '')}",
|
||||
"head": head_branch,
|
||||
"base": base_branch,
|
||||
"priority_id": "2",
|
||||
"is_original": False,
|
||||
"fork_project_id": "",
|
||||
"receivers_login": [],
|
||||
"merge_project_identifier": ""
|
||||
},
|
||||
# 配置2:尝试不发送fork相关字段
|
||||
{
|
||||
"title": pull_data.get("title", "未命名PR"),
|
||||
"body": f"从Gitee同步的PR\n\n原始PR信息:\n- 标题: {pull_data.get('title', 'N/A')}\n- 原始链接: {pull_data.get('html_url', 'N/A')}\n- 状态: {pull_data.get('state', 'N/A')}\n\n{pull_data.get('body', '')}",
|
||||
"head": head_branch,
|
||||
"base": base_branch,
|
||||
"priority_id": "2",
|
||||
"receivers_login": []
|
||||
},
|
||||
# 配置3:minimal payload,只包含必需字段
|
||||
{
|
||||
"title": pull_data.get("title", "未命名PR"),
|
||||
"body": f"从Gitee同步的PR\n\n{pull_data.get('body', '')}",
|
||||
"head": head_branch,
|
||||
"base": base_branch,
|
||||
"priority_id": "2",
|
||||
"is_original": False,
|
||||
"fork_project_id": "",
|
||||
"receivers_login": [],
|
||||
"merge_project_identifier": ""
|
||||
}
|
||||
]
|
||||
|
||||
# 打印创建信息
|
||||
print(f" - 标题: {pull_data.get('title', '未命名PR')}")
|
||||
print(f" - 源分支: {head_branch}")
|
||||
print(f" - 目标分支: {base_branch}")
|
||||
print(f" - 请求URL: {url}")
|
||||
|
||||
# 尝试不同的配置
|
||||
for i, payload in enumerate(payloads_to_try):
|
||||
print(f" - 尝试配置 {i+1}...")
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
print(f" 状态码: {response.status_code}")
|
||||
if response.text:
|
||||
print(f" 响应: {response.text[:300]}")
|
||||
|
||||
# 处理响应
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("status") == 0:
|
||||
print(f" ✓ 创建成功: PR #{data.get('pull_request_id')}")
|
||||
return data
|
||||
else:
|
||||
print(f" API错误: {data.get('message', '未知错误')}")
|
||||
# 继续尝试下一个配置
|
||||
continue
|
||||
else:
|
||||
print(f" HTTP错误: {response.status_code}")
|
||||
# 继续尝试下一个配置
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f" 异常: {str(e)}")
|
||||
# 继续尝试下一个配置
|
||||
continue
|
||||
|
||||
# 所有配置都失败了
|
||||
print(f" ✗ 所有配置都失败了")
|
||||
return None
|
||||
|
||||
def check_can_sync_to_gitlink(pull_data):
|
||||
"""检查PR是否可以同步到Gitlink,返回(可同步, 原因)"""
|
||||
# 处理分支名称
|
||||
head_branch = pull_data.get("head", {}).get("ref", "").replace("refs/heads/", "")
|
||||
base_branch = pull_data.get("base", {}).get("ref", "").replace("refs/heads/", "")
|
||||
|
||||
# 如果分支名称为空,使用默认值
|
||||
if not head_branch:
|
||||
return False, "源分支名称为空"
|
||||
if not base_branch:
|
||||
return False, "目标分支名称为空"
|
||||
|
||||
# 检查是否已存在相同分支的PR
|
||||
global SOURCE_REPO, GITLINK_COOKIE
|
||||
owner, repo = SOURCE_REPO.split('/')
|
||||
url = f"https://www.gitlink.org.cn/api/v1/{owner}/{repo}/pulls.json"
|
||||
headers = {
|
||||
"Cookie": f"autologin_trustie={GITLINK_COOKIE}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
pulls = response.json().get("pulls", [])
|
||||
for pull in pulls:
|
||||
if (pull.get("head") == head_branch and
|
||||
pull.get("base") == base_branch and
|
||||
pull.get("state") == "open"):
|
||||
return False, f"已存在相同分支的PR: #{pull.get('id')} {pull.get('title')}"
|
||||
except Exception as e:
|
||||
print(f"检查Gitlink现有PR失败: {str(e)}")
|
||||
|
||||
return True, "可以同步"
|
||||
|
||||
def sync_gitee_to_gitlink():
|
||||
"""从Gitee同步PR到Gitlink"""
|
||||
print("开始从Gitee同步PR到Gitlink...")
|
||||
|
||||
# 加载已有的反向同步映射
|
||||
sync_mapping = load_sync_mapping(REVERSE_SYNC_MAPPING_FILE)
|
||||
|
||||
# 获取Gitee的PR列表
|
||||
pulls = get_gitee_pulls()
|
||||
if not pulls:
|
||||
print("没有获取到Gitee的PR列表")
|
||||
return
|
||||
|
||||
print(f"获取到 {len(pulls)} 个Gitee PR")
|
||||
|
||||
# 筛选可同步的PR
|
||||
syncable_pulls = []
|
||||
for pull in pulls:
|
||||
gitee_pr_id = pull.get("id")
|
||||
|
||||
# 检查是否已同步
|
||||
if str(gitee_pr_id) in sync_mapping:
|
||||
continue
|
||||
|
||||
# 检查是否可同步
|
||||
can_sync, reason = check_can_sync_to_gitlink(pull)
|
||||
if can_sync:
|
||||
syncable_pulls.append(pull)
|
||||
else:
|
||||
# 可以选择在调试模式下输出不可同步的原因
|
||||
# print(f"PR #{gitee_pr_id} ({pull.get('title')}) 不符合同步条件: {reason}")
|
||||
pass
|
||||
|
||||
if not syncable_pulls:
|
||||
print("没有发现可同步的PR")
|
||||
return
|
||||
|
||||
print(f"发现 {len(syncable_pulls)} 个可同步的PR:")
|
||||
for i, pull in enumerate(syncable_pulls):
|
||||
head_branch = pull.get("head", {}).get("ref", "").replace("refs/heads/", "")
|
||||
base_branch = pull.get("base", {}).get("ref", "").replace("refs/heads/", "")
|
||||
print(f"{i+1}. PR #{pull.get('number')} - {pull.get('title')} (源分支: {head_branch}, 目标分支: {base_branch})")
|
||||
|
||||
# 同步每个PR
|
||||
success_count = 0
|
||||
for pull in syncable_pulls:
|
||||
gitee_pr_id = pull.get("id")
|
||||
gitee_pr_number = pull.get("number")
|
||||
|
||||
print(f"\n正在同步PR #{gitee_pr_number}: {pull.get('title', 'Unknown')}")
|
||||
|
||||
# 在Gitlink创建PR
|
||||
gitlink_pr = create_gitlink_pull(pull)
|
||||
if not gitlink_pr:
|
||||
print(f"PR #{gitee_pr_number} 同步失败")
|
||||
continue
|
||||
|
||||
gitlink_pr_id = gitlink_pr.get("pull_request_id")
|
||||
print(f"✓ 成功创建Gitlink PR #{gitlink_pr_id}")
|
||||
success_count += 1
|
||||
|
||||
# 保存同步映射
|
||||
sync_mapping[str(gitee_pr_id)] = gitlink_pr_id
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(1)
|
||||
|
||||
# 保存同步映射
|
||||
save_sync_mapping(sync_mapping, REVERSE_SYNC_MAPPING_FILE)
|
||||
print(f"\n反向同步完成!成功: {success_count}/{len(syncable_pulls)} 个PR")
|
||||
|
||||
def sync_gitlink_to_gitee():
|
||||
"""从Gitlink同步PR到Gitee"""
|
||||
# 首先测试API连接
|
||||
if not test_gitee_api():
|
||||
print("API测试失败,请检查配置")
|
||||
return
|
||||
|
||||
# 加载已有的同步映射
|
||||
sync_mapping = load_sync_mapping(SYNC_MAPPING_FILE)
|
||||
|
||||
# 获取Gitlink的PR列表
|
||||
pulls = get_gitlink_pulls()
|
||||
if not pulls:
|
||||
print("没有获取到Gitlink的PR列表")
|
||||
return
|
||||
|
||||
print(f"\n获取到 {len(pulls)} 个Gitlink PR")
|
||||
|
||||
# 筛选可同步的PR
|
||||
syncable_pulls = []
|
||||
for pull in pulls:
|
||||
gitlink_pr_id = pull.get("id")
|
||||
|
||||
# 检查是否已同步
|
||||
if str(gitlink_pr_id) in sync_mapping:
|
||||
continue
|
||||
|
||||
# 检查是否可同步
|
||||
can_sync, reason = check_can_sync_pull_request(pull)
|
||||
if can_sync:
|
||||
syncable_pulls.append(pull)
|
||||
else:
|
||||
# 可以选择在调试模式下输出不可同步的原因
|
||||
# print(f"PR #{gitlink_pr_id} ({pull.get('title')}) 不符合同步条件: {reason}")
|
||||
pass
|
||||
|
||||
if not syncable_pulls:
|
||||
print("没有发现可同步的PR")
|
||||
return
|
||||
|
||||
print(f"\n发现 {len(syncable_pulls)} 个可同步的PR:")
|
||||
for i, pull in enumerate(syncable_pulls):
|
||||
print(f"{i+1}. PR #{pull.get('id')} - {pull.get('title')} (源分支: {pull.get('head')}, 目标分支: {pull.get('base')})")
|
||||
|
||||
# 同步每个PR
|
||||
success_count = 0
|
||||
for pull in syncable_pulls:
|
||||
gitlink_pr_id = pull.get("id")
|
||||
|
||||
print(f"\n正在同步PR #{gitlink_pr_id}: {pull.get('title', 'Unknown')}")
|
||||
|
||||
# 在Gitee创建PR
|
||||
gitee_pr = create_gitee_pull(pull)
|
||||
if not gitee_pr:
|
||||
print(f"PR #{gitlink_pr_id} 同步失败")
|
||||
continue
|
||||
|
||||
gitee_pr_id = gitee_pr.get("id") or gitee_pr.get("number")
|
||||
print(f"✓ 成功创建Gitee PR #{gitee_pr_id}")
|
||||
success_count += 1
|
||||
|
||||
# 保存同步映射
|
||||
sync_mapping[str(gitlink_pr_id)] = gitee_pr_id
|
||||
|
||||
sync_pull_comments(gitlink_pr_id, gitee_pr_id)
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(1)
|
||||
|
||||
# 保存同步映射
|
||||
save_sync_mapping(sync_mapping, SYNC_MAPPING_FILE)
|
||||
print(f"\n同步完成!成功: {success_count}/{len(syncable_pulls)} 个PR")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='双向同步PR')
|
||||
parser.add_argument('--direction', choices=['to_gitee', 'to_gitlink'], default='to_gitee',
|
||||
help='同步方向: to_gitee (从Gitlink到Gitee), to_gitlink (从Gitee到Gitlink)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.direction == 'to_gitee':
|
||||
sync_gitlink_to_gitee()
|
||||
elif args.direction == 'to_gitlink':
|
||||
sync_gitee_to_gitlink()
|
||||
else:
|
||||
print("无效的同步方向,使用 --help 查看帮助")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,15 +2,17 @@ import { useState } from 'react';
|
|||
import { history, useModel } from 'umi';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { QueryFilter, ProFormText } from '@ant-design/pro-form';
|
||||
import { Button, Table, Card, Modal, Divider, Tooltip, message } from 'antd';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { QueryFilter, ProFormText, ProFormSelect, ModalForm } from '@ant-design/pro-form';
|
||||
import { Button, Table, Card, Modal, Divider, Tooltip, message, Space } from 'antd';
|
||||
import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import useRequest from '@/utils/useRequest';
|
||||
import {
|
||||
listPullRequest,
|
||||
approvePullRequest,
|
||||
mergePullRequest,
|
||||
pressPullRequest,
|
||||
crossSyncPR,
|
||||
syncPullRequest,
|
||||
} from '@/services/Pullrequests';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -25,6 +27,7 @@ export default (props: Props) => {
|
|||
const { name } = props?.location?.query || {};
|
||||
const { initSearchParams } = useModel('global');
|
||||
const [searchParams, setSearchParams] = useState<API.SearchParams>(initSearchParams);
|
||||
const [crossSyncVisible, setCrossSyncVisible] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
run: fetchPullRequest,
|
||||
|
|
@ -41,6 +44,8 @@ export default (props: Props) => {
|
|||
const { run: handleApprove } = useRequest(approvePullRequest);
|
||||
const { run: handleMerge } = useRequest(mergePullRequest);
|
||||
const { run: handlePress } = useRequest(pressPullRequest);
|
||||
const { run: handleSync } = useRequest(syncPullRequest);
|
||||
const { run: handleCrossSync, loading: crossSyncLoading } = useRequest(crossSyncPR);
|
||||
|
||||
const handleActions = (id: number, type: string) => {
|
||||
let request: any;
|
||||
|
|
@ -72,6 +77,23 @@ export default (props: Props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleSyncPRs = () => {
|
||||
if (!name) return;
|
||||
Modal.confirm({
|
||||
title: `确定同步当前项目的PR吗?`,
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
onOk: async () => {
|
||||
handleSync({ name }).then(({ success }: API.Response) => {
|
||||
if (success) {
|
||||
message?.destroy();
|
||||
message.success('PR同步成功');
|
||||
loadData();
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = ({ current, pageSize }: { current: number, pageSize: number }) => {
|
||||
loadData({ ...searchParams, pageNum: current, pageSize });
|
||||
}
|
||||
|
|
@ -124,9 +146,21 @@ export default (props: Props) => {
|
|||
ghost
|
||||
header={{ title: name }}
|
||||
extra={
|
||||
<Button onClick={() => history.goBack()} key="go_back">
|
||||
返回
|
||||
</Button>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => setCrossSyncVisible(true)}
|
||||
>
|
||||
跨平台PR同步
|
||||
</Button>
|
||||
<Button onClick={handleSyncPRs}>
|
||||
同步PR
|
||||
</Button>
|
||||
<Button onClick={() => history.goBack()} key="go_back">
|
||||
返回
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Card className="searchCard">
|
||||
|
|
@ -158,6 +192,69 @@ export default (props: Props) => {
|
|||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 跨平台PR同步弹窗 */}
|
||||
<ModalForm
|
||||
title="跨平台PR同步"
|
||||
visible={crossSyncVisible}
|
||||
onVisibleChange={setCrossSyncVisible}
|
||||
onFinish={async (values) => {
|
||||
if (!name) return false;
|
||||
|
||||
const params = {
|
||||
name,
|
||||
...values,
|
||||
};
|
||||
|
||||
const res = await handleCrossSync(params);
|
||||
if (res.success) {
|
||||
message.success(res.msg || '同步成功');
|
||||
loadData();
|
||||
return true;
|
||||
} else {
|
||||
message.error(res.msg || '同步失败');
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
submitter={{
|
||||
submitButtonProps: {
|
||||
loading: crossSyncLoading,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="source_repo"
|
||||
label="源仓库地址"
|
||||
placeholder="请输入源仓库地址,格式:owner/repo"
|
||||
rules={[{ required: true, message: '请输入源仓库地址' }]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="target_repo"
|
||||
label="目标仓库地址"
|
||||
placeholder="请输入目标仓库地址,格式:owner/repo"
|
||||
rules={[{ required: true, message: '请输入目标仓库地址' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="direction"
|
||||
label="同步方向"
|
||||
options={[
|
||||
{ label: 'Gitlink -> Gitee', value: 'to_gitee' },
|
||||
{ label: 'Gitee -> Gitlink', value: 'to_gitlink' },
|
||||
]}
|
||||
initialValue="to_gitee"
|
||||
rules={[{ required: true, message: '请选择同步方向' }]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="gitlink_cookie"
|
||||
label="Gitlink Cookie"
|
||||
placeholder="可选,不填则使用系统配置"
|
||||
/>
|
||||
<ProFormText
|
||||
name="gitee_token"
|
||||
label="Gitee Token"
|
||||
placeholder="可选,不填则使用系统配置"
|
||||
/>
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -121,3 +121,35 @@ export async function pressPullRequest(
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** crossSyncPR 跨平台同步PR POST /cerobot/projects/${param0}/cross-sync-pr */
|
||||
export async function crossSyncPR(
|
||||
params: {
|
||||
// path
|
||||
name: string;
|
||||
// query
|
||||
/** 源仓库地址(格式:owner/repo) */
|
||||
source_repo: string;
|
||||
/** 目标仓库地址(格式:owner/repo) */
|
||||
target_repo: string;
|
||||
/** 同步方向,可选值:to_gitee(Gitlink到Gitee)或to_gitlink(Gitee到Gitlink) */
|
||||
direction?: 'to_gitee' | 'to_gitlink';
|
||||
/** Gitlink Cookie */
|
||||
gitlink_cookie?: string;
|
||||
/** Gitee Token */
|
||||
gitee_token?: string;
|
||||
},
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { name: param0, ...queryParams } = params;
|
||||
return request<API.Response>(
|
||||
`/cerobot/projects/${param0}/cross-sync-pr`,
|
||||
{
|
||||
method: 'POST',
|
||||
params: {
|
||||
...queryParams,
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue