RAG/utils/git_tool.py

325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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