Git仓库代码库解析到知识库
2024年5月15日...大约 7 分钟
一、介绍
扩展本地自定义知识库的解析功能,新增 Git 仓库解析。用户输入 Git 仓库地址和账号密码,即可拉取代码并上传至知识库,随后便可基于此代码进行使用。
二、技术方案
将 JGit 操作库引入工程,用于执行 Git 命令拉取代码仓库。随后遍历代码库文件,依次解析、分割并上传至向量库。
三、具体实现
1. 项目结构

2. 引入组件
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>7.2.0.202503040940-r</version>
</dependency>
- 描述: JGit 是一个纯 Java 实现的 Git 版本控制系统。它不需要本地安装 Git 客户端。
- 优点:
- 纯 Java,无需依赖外部 Git 命令。
- 跨平台兼容性好。
- API 稳定且完善。
- 活跃的社区和良好的文档。
- 是 Eclipse 基金会的一部分,质量有保证。
3. 解析仓库
/**
* 处理分析 Git 仓库的 POST 请求。
* 克隆指定的 Git 仓库,遍历其中的文件,解析内容,分割文本,
* 添加元数据,并将处理后的文档存储到向量数据库中。
* 同时,将仓库项目名添加到 Redis 列表中作为标签。
*
* @param repoUrl Git 仓库的 URL (例如: https://github.com/user/repo.git)
* @param userName 用于访问私有仓库的用户名 (如果仓库是公开的,可能不需要或使用空字符串)
* @param token 用于访问私有仓库的访问令牌或密码
* @return 包含操作结果的响应对象 (成功时 code="1000")
* @throws Exception 可能在 Git 操作、文件 IO、文档处理或数据库交互中抛出异常
*/
@PostMapping(value = "analyze_git_repository")
// @Override // 如果是从接口继承,保留此注解
public Response<String> analyzeGitRepository(@RequestParam String repoUrl, @RequestParam String userName, @RequestParam String token) throws Exception {
// 定义本地克隆路径
String localPath = "./git-cloned-repo";
// 从仓库 URL 提取项目名称 (例如: "repo")
String repoProjectName = extractProjectName(repoUrl);
// 记录日志:显示将要克隆到的绝对路径
log.info("克隆路径:{}", new File(localPath).getAbsolutePath());
// 清理旧的克隆目录:确保每次都是全新克隆,防止旧文件干扰
// FileUtils.deleteDirectory 需要 commons-io 依赖
try {
FileUtils.deleteDirectory(new File(localPath));
log.info("已删除旧的克隆目录: {}", localPath);
} catch (IOException e) {
log.warn("删除旧的克隆目录失败: {}, 可能目录不存在或权限问题.", localPath, e);
// 根据需要决定是否继续,这里选择继续尝试克隆
}
// 克隆 Git 仓库
Git git = null;
try {
git = Git.cloneRepository()
.setURI(repoUrl) // 设置远程仓库 URL
.setDirectory(new File(localPath)) // 设置本地克隆目录
// 设置凭证,用于访问需要认证的仓库 (如私有库)
.setCredentialsProvider(new UsernamePasswordCredentialsProvider(userName, token))
.call(); // 执行克隆操作
log.info("成功克隆仓库: {}", repoUrl);
// 遍历克隆下来的仓库文件系统
Files.walkFileTree(Paths.get(localPath), new SimpleFileVisitor<>() {
/**
* 访问每个文件时调用此方法。
* @param file 当前访问的文件路径
* @param attrs 文件的基本属性
* @return 控制文件遍历行为 (CONTINUE 表示继续遍历)
* @throws IOException IO 异常
*/
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 忽略 .git 目录下的文件
if (file.toString().contains(File.separator + ".git" + File.separator)) {
return FileVisitResult.CONTINUE;
}
// 记录日志:正在处理哪个文件
log.info("{} 遍历解析路径,上传知识库: {}", repoProjectName, file.getFileName());
try {
// 使用 TikaDocumentReader 读取和解析文件内容
// PathResource 用于包装文件路径
TikaDocumentReader reader = new TikaDocumentReader(new PathResource(file));
// 获取解析后的文档列表 (Tika 可能将一个文件解析为多个 Document)
List<Document> documents = reader.get();
// 如果文档内容为空,则跳过处理
if (documents == null || documents.isEmpty() || documents.stream().allMatch(doc -> doc.getContent() == null || doc.getContent().isBlank())) {
log.warn("文件内容为空或无法解析,跳过: {}", file.getFileName());
return FileVisitResult.CONTINUE;
}
// 使用 TokenTextSplitter 将文档分割成更小的块
// 这有助于向量化和检索,因为模型通常有输入长度限制
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
// --- 元数据添加逻辑 ---
// 注意:这里对原始 documents 和分割后的 documentSplitterList 都添加了元数据。
// 通常只需要对最终要存入 VectorStore 的 documentSplitterList 添加元数据即可。
// 保留原始代码逻辑,但添加注释说明。
// 为原始文档添加元数据 (这部分可能非必需,除非原始文档也用于其他目的)
documents.forEach(doc -> doc.getMetadata().put("cactusli", repoProjectName)); // "cactusli" 可能是项目特定的元数据键
// 为分割后的文档块添加元数据,键为 "cactusli",值为仓库项目名
// 这个元数据可以用于后续检索时按项目进行过滤
documentSplitterList.forEach(doc -> doc.getMetadata().put("cactusli", repoProjectName));
// 将分割后的文档块列表提交给 VectorStore 进行处理和存储
// VectorStore 通常会进行文本嵌入(向量化)并存入数据库
pgVectorStore.accept(documentSplitterList);
} catch (Exception e) {
// 记录处理单个文件时发生的错误,并继续处理下一个文件
log.error("遍历解析路径,上传知识库失败: {} - 错误: {}", file.getFileName(), e.getMessage(), e);
}
// 继续遍历文件树
return FileVisitResult.CONTINUE;
}
/**
* 访问文件失败时调用此方法。
* @param file 访问失败的文件路径
* @param exc 发生的 IO 异常
* @return 控制文件遍历行为 (CONTINUE 表示忽略错误并继续遍历)
* @throws IOException IO 异常
*/
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
// 记录访问文件失败的日志
log.warn("访问文件失败: {} - {}", file.toString(), exc.getMessage());
// 忽略错误,继续遍历其他文件
return FileVisitResult.CONTINUE;
}
});
// 处理完成后,再次删除本地克隆的仓库目录,进行清理
FileUtils.deleteDirectory(new File(localPath));
log.info("已删除处理后的克隆目录: {}", localPath);
// 使用 Redisson 将项目名称添加到一个 Redis List 中,作为 RAG 的标签
// 这可以用于跟踪已处理的仓库或在 RAG 查询时提供可选的过滤标签
RList<String> elements = redissonClient.getList("ragTag"); // 获取名为 "ragTag" 的 Redis 列表
// 如果列表中不包含当前项目名,则添加
if (!elements.contains(repoProjectName)) {
elements.add(repoProjectName);
log.info("已将项目标签 '{}' 添加到 Redis 列表 'ragTag'", repoProjectName);
} else {
log.info("项目标签 '{}' 已存在于 Redis 列表 'ragTag'", repoProjectName);
}
} catch (Exception e) {
// 捕获克隆或文件处理过程中的主要异常
log.error("处理 Git 仓库时发生错误: {}", repoUrl, e);
// 抛出异常,让全局异常处理器或调用者处理
throw e; // 或者可以返回一个表示错误的 Response 对象
} finally {
// 确保 Git 对象被关闭,释放资源 (例如文件句柄)
if (git != null) {
git.close();
}
// 尝试再次清理本地目录,以防处理中途失败未清理
try {
FileUtils.deleteDirectory(new File(localPath));
} catch (IOException e) {
log.warn("最终清理克隆目录失败: {}", localPath, e);
}
}
// 记录整个仓库处理完成的日志
log.info("遍历解析路径,上传完成: {}", repoUrl);
// 构建并返回成功的响应
// 假设 Response 是一个包含 code 和 info 字段的自定义类
return Response.<String>builder().code("1000").info("调用成功").build();
}
/**
* 从 Git 仓库 URL 中提取项目名称。
* 例如,从 "https://github.com/user/my-repo.git" 提取 "my-repo"。
*
* @param repoUrl Git 仓库的 URL
* @return 提取出的项目名称 (移除了 ".git" 后缀)
*/
private String extractProjectName(String repoUrl) {
// 按 "/" 分割 URL
String[] parts = repoUrl.split("/");
// 获取最后一部分,通常是 "项目名.git"
String projectNameWithGit = parts[parts.length - 1];
// 移除 ".git" 后缀得到纯项目名
return projectNameWithGit.replace(".git", "");
}
大致描述此方法核心的三个流程;
增加 analyze_git_repository 解析 Git 仓库方法。此方法,会把用户提交的仓库地址和账号信息,进行仓库克隆。
完成后在依次遍历仓库内的文件,打标知识库。上传到向量库中。
- 最后把知识库标签填写到 Redis 中。
验证知识库是否上传成功可以在日志中查看,然后再看是否存入到了数据库。
赞助