fix: 修复 Gitee 文件推送问题
主要修复: 1. 修复 Gitea API 返回数组的处理 2. 修复获取 commit tree SHA 的路径问题 3. 添加空仓库初始提交支持 4. 添加 API 限流重试机制 5. 改为不自动初始化仓库,使用 Git Data API 推送初始提交 当前状态: Gitee 服务器严重 API 限流 (429)
This commit is contained in:
+288
-8
@@ -55,7 +55,8 @@ class GiteeManager:
|
|||||||
self,
|
self,
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
private: bool = False
|
private: bool = False,
|
||||||
|
auto_init: bool = False
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
创建 Gitee 仓库
|
创建 Gitee 仓库
|
||||||
@@ -64,6 +65,7 @@ class GiteeManager:
|
|||||||
repo_name: 仓库名称
|
repo_name: 仓库名称
|
||||||
description: 仓库描述
|
description: 仓库描述
|
||||||
private: 是否私有
|
private: 是否私有
|
||||||
|
auto_init: 是否自动初始化(默认不初始化,这样可以直接推送文件)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
仓库信息
|
仓库信息
|
||||||
@@ -74,10 +76,14 @@ class GiteeManager:
|
|||||||
"name": repo_name,
|
"name": repo_name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"private": private,
|
"private": private,
|
||||||
"auto_init": True, # 自动初始化
|
"auto_init": auto_init, # 不自动初始化,后续直接推送文件
|
||||||
"default_branch": "main"
|
"default_branch": "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import time
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url,
|
url,
|
||||||
@@ -110,15 +116,29 @@ class GiteeManager:
|
|||||||
"html_url": f"{self.gitee_base_url}/{owner}/{repo_name}",
|
"html_url": f"{self.gitee_base_url}/{owner}/{repo_name}",
|
||||||
"exists": True
|
"exists": True
|
||||||
}
|
}
|
||||||
|
elif response.status_code == 429:
|
||||||
|
# API 限流,等待后重试
|
||||||
|
wait_time = (attempt + 1) * 2
|
||||||
|
logger.warning(f"API 限流,等待 {wait_time} 秒后重试 ({attempt + 1}/{max_retries})")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
error_msg = response.json().get("message", response.text)
|
error_msg = response.json().get("message", response.text)
|
||||||
|
except:
|
||||||
|
error_msg = response.text[:200] if response.text else f"HTTP {response.status_code}"
|
||||||
logger.error(f"创建仓库失败: {error_msg}")
|
logger.error(f"创建仓库失败: {error_msg}")
|
||||||
return {"success": False, "error": error_msg}
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"创建仓库异常: {e}")
|
logger.error(f"创建仓库异常 (尝试 {attempt + 1}/{max_retries}): {e}")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
return {"success": False, "error": "API 限流,请稍后重试"}
|
||||||
|
|
||||||
def push_files(
|
def push_files(
|
||||||
self,
|
self,
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
@@ -128,7 +148,7 @@ class GiteeManager:
|
|||||||
owner: str = None
|
owner: str = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
推送文件到仓库
|
推送文件到仓库(使用 Git Data API 批量提交)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repo_name: 仓库名称
|
repo_name: 仓库名称
|
||||||
@@ -146,19 +166,51 @@ class GiteeManager:
|
|||||||
repo_owner = owner or self.gitee_username or self.gitee_owner
|
repo_owner = owner or self.gitee_username or self.gitee_owner
|
||||||
results = {"success": True, "files": [], "owner": repo_owner}
|
results = {"success": True, "files": [], "owner": repo_owner}
|
||||||
|
|
||||||
|
logger.info(f"开始推送 {len(files)} 个文件到 {repo_owner}/{repo_name}")
|
||||||
|
|
||||||
|
# 检查仓库是否为空
|
||||||
|
is_empty = self._is_repo_empty(repo_owner, repo_name)
|
||||||
|
|
||||||
|
if is_empty:
|
||||||
|
# 空仓库:使用初始提交方式
|
||||||
|
logger.info("仓库为空,使用初始提交方式")
|
||||||
|
init_result = self._push_initial_commit(repo_owner, repo_name, files, commit_message, branch)
|
||||||
|
if init_result.get("success"):
|
||||||
|
logger.info(f"✅ 初始提交成功: {len(files)} 个文件")
|
||||||
|
results["files"] = [{"path": p, "status": "success"} for p in files.keys()]
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
logger.error(f"初始提交失败: {init_result.get('error')}")
|
||||||
|
results["success"] = False
|
||||||
|
results["error"] = init_result.get("error")
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 非空仓库:尝试使用批量提交 API
|
||||||
|
try:
|
||||||
|
batch_result = self._push_files_batch(repo_owner, repo_name, files, commit_message, branch)
|
||||||
|
if batch_result.get("success"):
|
||||||
|
logger.info(f"✅ 批量推送成功: {len(files)} 个文件")
|
||||||
|
results["files"] = [{"path": p, "status": "success"} for p in files.keys()]
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
logger.warning(f"批量推送失败,尝试逐个推送: {batch_result.get('error')}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"批量推送异常,尝试逐个推送: {e}")
|
||||||
|
|
||||||
|
# 方法2:逐个推送文件
|
||||||
for file_path, content in files.items():
|
for file_path, content in files.items():
|
||||||
try:
|
try:
|
||||||
# 使用 Contents API 创建/更新文件
|
# 使用 Contents API 创建/更新文件
|
||||||
url = f"{self.gitee_api_url}/repos/{repo_owner}/{repo_name}/contents/{file_path}"
|
url = f"{self.gitee_api_url}/repos/{repo_owner}/{repo_name}/contents/{file_path}"
|
||||||
|
|
||||||
# 检查文件是否存在
|
# 检查文件是否存在(获取 SHA)
|
||||||
check_response = requests.get(url, headers=self._get_headers(), timeout=10)
|
check_response = requests.get(url, headers=self._get_headers(), timeout=10)
|
||||||
sha = None
|
sha = None
|
||||||
if check_response.status_code == 200:
|
if check_response.status_code == 200:
|
||||||
sha = check_response.json().get("sha")
|
sha = check_response.json().get("sha")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"message": commit_message,
|
"message": f"{commit_message} - {file_path}",
|
||||||
"content": base64.b64encode(content.encode()).decode(),
|
"content": base64.b64encode(content.encode()).decode(),
|
||||||
"branch": branch
|
"branch": branch
|
||||||
}
|
}
|
||||||
@@ -174,7 +226,7 @@ class GiteeManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 添加短暂延迟避免 API 限流
|
# 添加短暂延迟避免 API 限流
|
||||||
time.sleep(0.5)
|
time.sleep(0.3)
|
||||||
|
|
||||||
if response.status_code in [200, 201]:
|
if response.status_code in [200, 201]:
|
||||||
results["files"].append({
|
results["files"].append({
|
||||||
@@ -183,14 +235,17 @@ class GiteeManager:
|
|||||||
})
|
})
|
||||||
logger.info(f"✅ 文件推送成功: {file_path}")
|
logger.info(f"✅ 文件推送成功: {file_path}")
|
||||||
else:
|
else:
|
||||||
|
error_msg = response.text[:200]
|
||||||
|
logger.error(f"❌ 文件推送失败: {file_path}, 状态码: {response.status_code}, 错误: {error_msg}")
|
||||||
results["files"].append({
|
results["files"].append({
|
||||||
"path": file_path,
|
"path": file_path,
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"error": response.text
|
"error": error_msg
|
||||||
})
|
})
|
||||||
results["success"] = False
|
results["success"] = False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 文件推送异常: {file_path}, 错误: {e}")
|
||||||
results["files"].append({
|
results["files"].append({
|
||||||
"path": file_path,
|
"path": file_path,
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
@@ -198,8 +253,233 @@ class GiteeManager:
|
|||||||
})
|
})
|
||||||
results["success"] = False
|
results["success"] = False
|
||||||
|
|
||||||
|
logger.info(f"推送完成: 成功 {sum(1 for f in results['files'] if f['status'] == 'success')}/{len(files)} 个文件")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def _push_files_batch(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo_name: str,
|
||||||
|
files: Dict[str, str],
|
||||||
|
commit_message: str,
|
||||||
|
branch: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
使用 Git Data API 批量推送文件
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 获取当前分支的最新 commit SHA
|
||||||
|
2. 获取该 commit 的 tree SHA
|
||||||
|
3. 创建新的 blobs
|
||||||
|
4. 创建新的 tree
|
||||||
|
5. 创建新的 commit
|
||||||
|
6. 更新分支引用
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
headers = self._get_headers()
|
||||||
|
base_url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}"
|
||||||
|
|
||||||
|
# 1. 获取当前分支的最新 commit
|
||||||
|
# Gitea API 返回的是数组
|
||||||
|
ref_url = f"{base_url}/git/refs/heads/{branch}"
|
||||||
|
ref_response = requests.get(ref_url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if ref_response.status_code != 200:
|
||||||
|
return {"success": False, "error": f"获取分支引用失败: {ref_response.text}"}
|
||||||
|
|
||||||
|
ref_data = ref_response.json()
|
||||||
|
|
||||||
|
# 处理 Gitea API 可能返回数组或对象的情况
|
||||||
|
if isinstance(ref_data, list):
|
||||||
|
if not ref_data:
|
||||||
|
return {"success": False, "error": "无法获取分支引用"}
|
||||||
|
ref_data = ref_data[0]
|
||||||
|
|
||||||
|
latest_commit_sha = ref_data.get("object", {}).get("sha")
|
||||||
|
|
||||||
|
if not latest_commit_sha:
|
||||||
|
return {"success": False, "error": "无法获取最新 commit SHA"}
|
||||||
|
|
||||||
|
logger.info(f"获取到最新 commit SHA: {latest_commit_sha[:8]}...")
|
||||||
|
|
||||||
|
# 2. 获取该 commit 的 tree
|
||||||
|
commit_url = f"{base_url}/git/commits/{latest_commit_sha}"
|
||||||
|
commit_response = requests.get(commit_url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if commit_response.status_code != 200:
|
||||||
|
return {"success": False, "error": f"获取 commit 信息失败: {commit_response.text}"}
|
||||||
|
|
||||||
|
commit_data = commit_response.json()
|
||||||
|
|
||||||
|
# Gitea API: tree 在 commit.tree 中
|
||||||
|
base_tree_sha = None
|
||||||
|
if commit_data.get("commit", {}).get("tree"):
|
||||||
|
base_tree_sha = commit_data["commit"]["tree"].get("sha")
|
||||||
|
elif commit_data.get("tree"):
|
||||||
|
base_tree_sha = commit_data["tree"].get("sha")
|
||||||
|
|
||||||
|
if not base_tree_sha:
|
||||||
|
logger.error(f"无法获取 tree SHA,commit 数据: {commit_data}")
|
||||||
|
return {"success": False, "error": "无法获取 tree SHA"}
|
||||||
|
|
||||||
|
logger.info(f"获取到 base tree SHA: {base_tree_sha[:8]}...")
|
||||||
|
|
||||||
|
# 3. 创建 tree 项目
|
||||||
|
tree_items = []
|
||||||
|
for file_path, content in files.items():
|
||||||
|
tree_items.append({
|
||||||
|
"path": file_path,
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"content": content
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 创建新 tree
|
||||||
|
tree_url = f"{base_url}/git/trees"
|
||||||
|
tree_data = {
|
||||||
|
"base_tree": base_tree_sha,
|
||||||
|
"tree": tree_items
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"创建新 tree,包含 {len(tree_items)} 个文件...")
|
||||||
|
tree_response = requests.post(tree_url, json=tree_data, headers=headers, timeout=120)
|
||||||
|
|
||||||
|
if tree_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"创建 tree 失败: {tree_response.text[:200]}"}
|
||||||
|
|
||||||
|
new_tree_sha = tree_response.json().get("sha")
|
||||||
|
logger.info(f"新 tree SHA: {new_tree_sha[:8]}...")
|
||||||
|
|
||||||
|
# 5. 创建新 commit
|
||||||
|
commit_create_url = f"{base_url}/git/commits"
|
||||||
|
commit_create_data = {
|
||||||
|
"message": commit_message,
|
||||||
|
"tree": new_tree_sha,
|
||||||
|
"parents": [latest_commit_sha]
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"创建新 commit...")
|
||||||
|
commit_create_response = requests.post(commit_create_url, json=commit_create_data, headers=headers, timeout=30)
|
||||||
|
|
||||||
|
if commit_create_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"创建 commit 失败: {commit_create_response.text[:200]}"}
|
||||||
|
|
||||||
|
new_commit_sha = commit_create_response.json().get("sha")
|
||||||
|
logger.info(f"新 commit SHA: {new_commit_sha[:8]}...")
|
||||||
|
|
||||||
|
# 6. 更新分支引用
|
||||||
|
update_ref_data = {
|
||||||
|
"sha": new_commit_sha,
|
||||||
|
"force": False
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"更新分支引用到新 commit...")
|
||||||
|
update_ref_response = requests.patch(ref_url, json=update_ref_data, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if update_ref_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"更新分支引用失败: {update_ref_response.text[:200]}"}
|
||||||
|
|
||||||
|
logger.info(f"✅ 批量推送成功,commit: {new_commit_sha[:8]}")
|
||||||
|
return {"success": True, "commit_sha": new_commit_sha}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
logger.error(f"批量推送异常: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def _is_repo_empty(self, owner: str, repo_name: str) -> bool:
|
||||||
|
"""检查仓库是否为空"""
|
||||||
|
try:
|
||||||
|
url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}"
|
||||||
|
response = requests.get(url, headers=self._get_headers(), timeout=10)
|
||||||
|
if response.status_code == 200:
|
||||||
|
repo_data = response.json()
|
||||||
|
return repo_data.get("empty", True)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"检查仓库是否为空失败: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _push_initial_commit(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo_name: str,
|
||||||
|
files: Dict[str, str],
|
||||||
|
commit_message: str,
|
||||||
|
branch: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
推送初始提交到空仓库
|
||||||
|
使用 Git Data API 创建初始 tree 和 commit
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
headers = self._get_headers()
|
||||||
|
base_url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}"
|
||||||
|
|
||||||
|
# 1. 创建 tree(不需要 base_tree,这是初始提交)
|
||||||
|
tree_items = []
|
||||||
|
for file_path, content in files.items():
|
||||||
|
tree_items.append({
|
||||||
|
"path": file_path,
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"content": content
|
||||||
|
})
|
||||||
|
|
||||||
|
tree_url = f"{base_url}/git/trees"
|
||||||
|
tree_data = {
|
||||||
|
"tree": tree_items
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"创建初始 tree,包含 {len(tree_items)} 个文件...")
|
||||||
|
tree_response = requests.post(tree_url, json=tree_data, headers=headers, timeout=120)
|
||||||
|
|
||||||
|
if tree_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"创建初始 tree 失败: {tree_response.text[:300]}"}
|
||||||
|
|
||||||
|
new_tree_sha = tree_response.json().get("sha")
|
||||||
|
logger.info(f"初始 tree SHA: {new_tree_sha[:8]}...")
|
||||||
|
|
||||||
|
# 2. 创建初始 commit(没有 parents)
|
||||||
|
commit_create_url = f"{base_url}/git/commits"
|
||||||
|
commit_create_data = {
|
||||||
|
"message": commit_message,
|
||||||
|
"tree": new_tree_sha
|
||||||
|
# 不设置 parents,这是初始提交
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"创建初始 commit...")
|
||||||
|
commit_create_response = requests.post(commit_create_url, json=commit_create_data, headers=headers, timeout=30)
|
||||||
|
|
||||||
|
if commit_create_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"创建初始 commit 失败: {commit_create_response.text[:300]}"}
|
||||||
|
|
||||||
|
new_commit_sha = commit_create_response.json().get("sha")
|
||||||
|
logger.info(f"初始 commit SHA: {new_commit_sha[:8]}...")
|
||||||
|
|
||||||
|
# 3. 创建分支引用
|
||||||
|
ref_url = f"{base_url}/git/refs"
|
||||||
|
ref_data = {
|
||||||
|
"ref": f"refs/heads/{branch}",
|
||||||
|
"sha": new_commit_sha
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"创建分支引用 {branch}...")
|
||||||
|
ref_response = requests.post(ref_url, json=ref_data, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if ref_response.status_code not in [200, 201]:
|
||||||
|
return {"success": False, "error": f"创建分支引用失败: {ref_response.text[:300]}"}
|
||||||
|
|
||||||
|
logger.info(f"✅ 初始提交成功,commit: {new_commit_sha[:8]}")
|
||||||
|
return {"success": True, "commit_sha": new_commit_sha}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
logger.error(f"初始提交异常: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def get_action_status(self, repo_name: str) -> Dict[str, Any]:
|
def get_action_status(self, repo_name: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取 Gitee Action 运行状态
|
获取 Gitee Action 运行状态
|
||||||
|
|||||||
Reference in New Issue
Block a user