325 lines
13 KiB
Python
325 lines
13 KiB
Python
"""
|
||
Git命令封装工具类
|
||
实现Git仓库的克隆、更新检测、增量拉取等功能
|
||
"""
|
||
import subprocess
|
||
import os
|
||
from typing import Tuple, Dict, List
|
||
from loguru import logger
|
||
from config import settings
|
||
|
||
|
||
class GitTool:
|
||
def __init__(self, user_id: str = "test", repo_id: str = "test", git_config: dict = None,
|
||
git_url: str = None, branch: str = None, protocol: str = None,
|
||
https_token: str = None, ssh_key: str = None, local_repo_path: str = None):
|
||
"""
|
||
初始化Git工具类
|
||
|
||
Args:
|
||
user_id: 用户ID
|
||
repo_id: 仓库ID
|
||
git_config: Git配置信息
|
||
git_url: Git仓库URL(直接参数,优先级高于git_config)
|
||
branch: Git分支(直接参数,优先级高于git_config)
|
||
protocol: Git协议(直接参数,优先级高于git_config)
|
||
https_token: HTTPS令牌(直接参数,优先级高于git_config)
|
||
ssh_key: SSH密钥(直接参数,优先级高于git_config)
|
||
local_repo_path: 本地仓库路径(直接参数,优先级高于git_config)
|
||
"""
|
||
self.user_id = user_id
|
||
self.repo_id = repo_id
|
||
|
||
# 优先使用直接参数,如果没有则使用git_config
|
||
if git_config:
|
||
self.git_url = git_url or git_config.get("git_url")
|
||
self.branch = branch or git_config.get("branch", settings.GIT_DEFAULT_BRANCH)
|
||
self.protocol = protocol or git_config.get("protocol", "https")
|
||
self.ssh_key = ssh_key or git_config.get("ssh_key")
|
||
self.https_token = https_token or git_config.get("https_token")
|
||
# 本地结构化存储路径
|
||
self.local_repo_path = local_repo_path or git_config.get("local_repo_path")
|
||
else:
|
||
self.git_url = git_url
|
||
self.branch = branch or settings.GIT_DEFAULT_BRANCH
|
||
self.protocol = protocol or "https"
|
||
self.ssh_key = ssh_key
|
||
self.https_token = https_token
|
||
self.local_repo_path = local_repo_path
|
||
|
||
if not self.local_repo_path:
|
||
self.local_repo_path = os.path.join(settings.GIT_LOCAL_STORAGE_ROOT, user_id, repo_id)
|
||
# 初始化Git环境
|
||
self._init_git_env()
|
||
|
||
def _init_git_env(self):
|
||
"""
|
||
初始化Git环境(SSH密钥配置)
|
||
"""
|
||
if self.ssh_key:
|
||
# 解密SSH私钥,写入临时文件,配置Git SSH
|
||
ssh_key_path = f"/tmp/ssh_key_{self.user_id}_{self.repo_id}"
|
||
with open(ssh_key_path, "w") as f:
|
||
f.write(self.ssh_key)
|
||
if not self.ssh_key.endswith('\n'):
|
||
f.write('\n')
|
||
os.chmod(ssh_key_path, 0o600)
|
||
os.environ["GIT_SSH_COMMAND"] = f"ssh -i {ssh_key_path} -o StrictHostKeyChecking=no"
|
||
|
||
def clone_repo(self) -> bool:
|
||
"""
|
||
克隆Git仓库
|
||
|
||
Returns:
|
||
bool: 是否成功克隆
|
||
"""
|
||
if not os.path.exists(self.local_repo_path):
|
||
os.makedirs(os.path.dirname(self.local_repo_path), exist_ok=True)
|
||
# 执行git clone命令
|
||
cmd = [
|
||
"git", "clone", "--single-branch",
|
||
"--branch", self.branch, self.git_url, self.local_repo_path
|
||
]
|
||
logger.info(f"执行Git克隆命令: {' '.join(cmd)}")
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if res.returncode != 0:
|
||
logger.error(f"Git克隆失败: {res.stderr}")
|
||
raise Exception(f"Git克隆失败: {res.stderr}")
|
||
# 克隆后校验
|
||
self._check_repo_integrity()
|
||
logger.info(f"Git仓库克隆成功: {self.local_repo_path}")
|
||
return True
|
||
logger.info(f"Git仓库已存在: {self.local_repo_path}")
|
||
return False
|
||
|
||
def _check_repo_integrity(self):
|
||
"""
|
||
仓库完整性校验(git fsck)+ 支持的编程语言检测
|
||
"""
|
||
# 执行git fsck
|
||
try:
|
||
subprocess.run(["git", "fsck"], cwd=self.local_repo_path, check=True, capture_output=True, text=True)
|
||
logger.info(f"Git仓库完整性校验成功: {self.local_repo_path}")
|
||
except subprocess.CalledProcessError as e:
|
||
logger.warning(f"Git仓库完整性校验失败: {e.stderr}")
|
||
|
||
# 扫描文件类型,记录支持的编程语言
|
||
support_lang = self._detect_support_lang()
|
||
logger.info(f"检测到支持的编程语言: {support_lang}")
|
||
return support_lang
|
||
|
||
def _detect_support_lang(self) -> List[str]:
|
||
"""
|
||
检测仓库支持的编程语言
|
||
|
||
Returns:
|
||
List[str]: 支持的编程语言列表
|
||
"""
|
||
lang_extensions = {
|
||
"python": [".py"],
|
||
"java": [".java"],
|
||
"go": [".go"],
|
||
"javascript": [".js", ".jsx"],
|
||
"typescript": [".ts", ".tsx"],
|
||
"c": [".c", ".h"],
|
||
"cpp": [".cpp", ".hpp", ".cc"],
|
||
"csharp": [".cs"],
|
||
"rust": [".rs"],
|
||
"php": [".php"],
|
||
"ruby": [".rb"],
|
||
"swift": [".swift"],
|
||
"kotlin": [".kt"],
|
||
"scala": [".scala"]
|
||
}
|
||
|
||
support_lang = []
|
||
for root, dirs, files in os.walk(self.local_repo_path):
|
||
# 跳过.git目录
|
||
if ".git" in dirs:
|
||
dirs.remove(".git")
|
||
# 跳过其他常见的非代码目录
|
||
dirs_to_skip = ["node_modules", "venv", "dist", "build", "__pycache__"]
|
||
dirs[:] = [d for d in dirs if d not in dirs_to_skip]
|
||
|
||
for file in files:
|
||
for lang, extensions in lang_extensions.items():
|
||
if any(file.endswith(ext) for ext in extensions):
|
||
if lang not in support_lang:
|
||
support_lang.append(lang)
|
||
break
|
||
|
||
return support_lang
|
||
|
||
def detect_remote_update(self) -> Tuple[bool, str, str]:
|
||
"""
|
||
远程更新检测
|
||
|
||
Returns:
|
||
Tuple[bool, str, str]: (是否有更新, 本地commit ID, 远程commit ID)
|
||
"""
|
||
# 确保仓库存在
|
||
if not os.path.exists(self.local_repo_path):
|
||
raise Exception(f"Git仓库不存在: {self.local_repo_path}")
|
||
|
||
# 拉取远程commit记录
|
||
try:
|
||
subprocess.run(["git", "fetch", "origin", f"{self.branch}:{self.branch}"],
|
||
cwd=self.local_repo_path, check=True, capture_output=True, text=True)
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"Git fetch失败: {e.stderr}")
|
||
raise
|
||
|
||
# 获取本地/远程commit ID
|
||
local_commit = subprocess.run(["git", "rev-parse", "HEAD"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.strip()
|
||
remote_commit = subprocess.run(["git", "rev-parse", f"origin/{self.branch}"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.strip()
|
||
|
||
has_update = local_commit != remote_commit
|
||
logger.info(f"Git更新检测: 本地={local_commit[:7]}, 远程={remote_commit[:7]}, 有更新={has_update}")
|
||
return has_update, local_commit, remote_commit
|
||
|
||
def incremental_pull(self, local_commit: str, remote_commit: str) -> Dict[str, List[str]]:
|
||
"""
|
||
增量拉取代码+解析文件变更
|
||
|
||
Args:
|
||
local_commit: 本地commit ID
|
||
remote_commit: 远程commit ID
|
||
|
||
Returns:
|
||
Dict[str, List[str]]: 文件变更集
|
||
"""
|
||
# 快进合并到远程最新版本
|
||
try:
|
||
subprocess.run(["git", "merge", "--ff-only", f"origin/{self.branch}"],
|
||
cwd=self.local_repo_path, check=True, capture_output=True, text=True)
|
||
logger.info(f"Git快进合并成功: {self.branch}")
|
||
except subprocess.CalledProcessError as e:
|
||
logger.error(f"Git合并失败: {e.stderr}")
|
||
raise
|
||
|
||
# 提取增量commit的文件变更
|
||
delta_commits = subprocess.run(["git", "log", "--pretty=format:%H", f"{local_commit}..{remote_commit}"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.split()
|
||
|
||
# 解析文件变更为ADD/MODIFY/DELETE
|
||
delta_files = self._parse_delta_files(delta_commits)
|
||
logger.info(f"Git增量变更: ADD={len(delta_files['ADD'])}, MODIFY={len(delta_files['MODIFY'])}, DELETE={len(delta_files['DELETE'])}")
|
||
return delta_files
|
||
|
||
def _parse_delta_files(self, delta_commits: List[str]) -> Dict[str, List[str]]:
|
||
"""
|
||
解析文件变更集
|
||
|
||
Args:
|
||
delta_commits: 增量commit列表
|
||
|
||
Returns:
|
||
Dict[str, List[str]]: 文件变更集
|
||
"""
|
||
add_files, modify_files, delete_files = [], [], []
|
||
|
||
for commit in delta_commits:
|
||
# git show --name-status 获取文件变更
|
||
res = subprocess.run(["git", "show", "--name-status", commit],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout
|
||
|
||
for line in res.splitlines():
|
||
if not line:
|
||
continue
|
||
# 解析状态和文件路径
|
||
if "\t" in line:
|
||
status, file_path = line.split("\t", 1)
|
||
full_path = os.path.join(self.local_repo_path, file_path)
|
||
if status == "A":
|
||
add_files.append(full_path)
|
||
elif status == "M":
|
||
modify_files.append(full_path)
|
||
elif status == "D":
|
||
delete_files.append(full_path)
|
||
|
||
# 去重并返回
|
||
return {
|
||
"ADD": list(set(add_files)),
|
||
"MODIFY": list(set(modify_files)),
|
||
"DELETE": list(set(delete_files))
|
||
}
|
||
|
||
def get_current_commit(self) -> str:
|
||
"""
|
||
获取当前commit ID
|
||
|
||
Returns:
|
||
str: 当前commit ID
|
||
"""
|
||
if not os.path.exists(self.local_repo_path):
|
||
raise Exception(f"Git仓库不存在: {self.local_repo_path}")
|
||
|
||
commit_id = subprocess.run(["git", "rev-parse", "HEAD"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.strip()
|
||
return commit_id
|
||
|
||
def get_repo_info(self) -> Dict[str, str]:
|
||
"""
|
||
获取仓库信息
|
||
|
||
Returns:
|
||
Dict[str, str]: 仓库信息
|
||
"""
|
||
if not os.path.exists(self.local_repo_path):
|
||
raise Exception(f"Git仓库不存在: {self.local_repo_path}")
|
||
|
||
# 获取仓库URL
|
||
remote_url = subprocess.run(["git", "config", "--get", "remote.origin.url"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.strip()
|
||
|
||
# 获取当前分支
|
||
current_branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||
cwd=self.local_repo_path, capture_output=True, text=True).stdout.strip()
|
||
|
||
# 获取当前commit
|
||
current_commit = self.get_current_commit()
|
||
|
||
return {
|
||
"remote_url": remote_url,
|
||
"current_branch": current_branch,
|
||
"current_commit": current_commit,
|
||
"local_path": self.local_repo_path
|
||
}
|
||
|
||
def test_connection(self) -> bool:
|
||
"""
|
||
测试Git连接
|
||
|
||
Returns:
|
||
bool: 是否连接成功
|
||
"""
|
||
if not self.git_url:
|
||
raise Exception("Git仓库URL未设置")
|
||
|
||
logger.info(f"测试Git连接: {self.git_url}")
|
||
|
||
# 尝试执行git ls-remote命令来测试连接
|
||
try:
|
||
cmd = ["git", "ls-remote", "--heads", self.git_url, f"refs/heads/{self.branch}"]
|
||
logger.info(f"执行Git连接测试命令: {' '.join(cmd)}")
|
||
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
if res.returncode == 0:
|
||
# 检查输出是否包含预期的分支信息
|
||
if self.branch in res.stdout:
|
||
logger.info("Git连接测试成功!")
|
||
return True
|
||
else:
|
||
logger.warning(f"Git连接测试失败:分支 {self.branch} 不存在")
|
||
return False
|
||
else:
|
||
logger.error(f"Git连接测试失败: {res.stderr}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Git连接测试异常: {e}")
|
||
return False
|