feat(core): 添加SVN日志查询工具和DeepSeek AI处理功能

- 实现SVN日志查询工具,支持版本范围和用户过滤
- 添加DeepSeek API集成,用于AI分析日志内容
- 创建Excel生成器,输出工作量统计报表
- 添加日志实体类和项目配置管理功能
- 集成POI库支持Excel文件操作
- 实现Markdown格式日志导出功能
This commit is contained in:
liumangmang
2026-02-05 09:11:17 +08:00
parent 25248a0275
commit a6817fd9bf
10 changed files with 1556 additions and 1 deletions

View File

@@ -0,0 +1,587 @@
package com.svnlog;
import okhttp3.*;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.*;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 使用DeepSeek API处理SVN日志并生成工作量统计Excel
*/
public class DeepSeekLogProcessor {
private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
private static final String API_KEY = "sk-48c59012c93b43a08fecbaf3e74799e7"; // 用户需要替换为实际的API Key
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(300, java.util.concurrent.TimeUnit.SECONDS) // 5分钟读取超时
.writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.build();
public static void main(String[] args) {
try {
Scanner scanner = new Scanner(System.in);
System.out.println("===========================================");
System.out.println(" SVN日志工作量统计工具DeepSeek版");
System.out.println(" 支持多项目汇总分析");
System.out.println("===========================================");
System.out.println();
// 读取markdown日志文件目录
System.out.print("请输入markdown日志文件所在目录路径 (回车使用当前目录): ");
String dirPath = scanner.nextLine().trim();
File dir;
if (dirPath.isEmpty()) {
dir = new File(".");
} else {
dir = new File(dirPath);
}
if (!dir.exists() || !dir.isDirectory()) {
System.err.println("错误: 目录不存在或不是有效目录!");
return;
}
// 扫描目录中的所有 .md 文件
File[] mdFiles = dir.listFiles((d, name) -> name.endsWith(".md"));
if (mdFiles == null || mdFiles.length == 0) {
System.err.println("错误: 目录中未找到任何 .md 文件!");
return;
}
System.out.println("找到 " + mdFiles.length + " 个日志文件:");
for (File file : mdFiles) {
System.out.println(" - " + file.getName());
}
System.out.println();
// 输入工作周期
SimpleDateFormat periodSdf = new SimpleDateFormat("yyyy年MM月");
String defaultPeriod = periodSdf.format(new Date());
System.out.print("请输入工作周期 (例如: 2025年12月回车使用默认: " + defaultPeriod + "): ");
String period = scanner.nextLine().trim();
if (period.isEmpty()) {
period = defaultPeriod;
System.out.println("使用默认工作周期: " + period);
}
// 读取并合并所有markdown文件
String combinedContent = readAndCombineMarkdownFiles(mdFiles);
System.out.println("成功读取并合并 " + mdFiles.length + " 个日志文件,总长度: " + combinedContent.length() + " 字符");
// 提示API Key
System.out.print("请输入DeepSeek API Key (留空使用代码中预设的): ");
String inputApiKey = scanner.nextLine().trim();
String apiKey = inputApiKey.isEmpty() ? API_KEY : inputApiKey;
if (apiKey.equals("YOUR_DEEPSEEK_API_KEY")) {
System.err.println("错误: 请提供有效的DeepSeek API Key");
return;
}
// 询问输出文件名
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
String defaultOutput = sdf.format(new Date()) + "工作量统计.xlsx";
System.out.print("请输入输出Excel文件名 (回车使用默认: " + defaultOutput + "): ");
String outputPath = scanner.nextLine().trim();
if (outputPath.isEmpty()) {
outputPath = defaultOutput;
}
System.out.println();
System.out.println("正在调用DeepSeek API分析日志...");
// 调用DeepSeek API处理日志
String prompt = buildPrompt(combinedContent, period);
String aiResponse = callDeepSeekAPI(apiKey, prompt);
if (aiResponse == null) {
System.err.println("DeepSeek API调用失败请检查网络连接和API Key。");
return;
}
if (aiResponse.isEmpty()) {
System.err.println("DeepSeek API返回空响应请重试或联系技术支持。");
return;
}
System.out.println("DeepSeek分析完成正在生成Excel...");
// 生成Excel
generateExcel(outputPath, aiResponse);
System.out.println();
System.out.println("Excel文件生成成功: " + outputPath);
System.out.println();
} catch (Exception e) {
System.err.println("发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 读取文件内容
*/
private static String readFile(String path) throws IOException {
return new String(Files.readAllBytes(new File(path).toPath()), "UTF-8");
}
/**
* 读取并合并多个markdown文件的内容
*/
private static String readAndCombineMarkdownFiles(File[] mdFiles) throws IOException {
StringBuilder combinedContent = new StringBuilder();
for (File file : mdFiles) {
String projectName = extractProjectName(file.getName());
String content = readFile(file.getAbsolutePath());
combinedContent.append("\n\n");
combinedContent.append("=== 项目: ").append(projectName).append(" ===\n");
combinedContent.append(content);
}
return combinedContent.toString();
}
/**
* 从文件名中提取项目名称
* 例如: svn_log_PRS-7050场站智慧管控_20260130_093348.md -> PRS-7050场站智慧管控
*/
private static String extractProjectName(String fileName) {
// 去掉 svn_log_ 前缀
if (fileName.startsWith("svn_log_")) {
fileName = fileName.substring(8);
}
// 去掉 .md 后缀
if (fileName.endsWith(".md")) {
fileName = fileName.substring(0, fileName.length() - 3);
}
// 去掉时间戳部分 (格式: _YYYYMMDD_HHMMSS)
int lastUnderscore = fileName.lastIndexOf('_');
if (lastUnderscore > 0) {
// 检查是否是时间戳格式
String timestampPart = fileName.substring(lastUnderscore + 1);
if (timestampPart.matches("\\d{8}_\\d{6}")) {
fileName = fileName.substring(0, lastUnderscore);
}
}
return fileName;
}
/**
* 构建发送给DeepSeek的提示词
*/
private static String buildPrompt(String markdownContent, String period) {
return "你是一个专业的项目管理助手。请分析以下多个项目的SVN日志并生成工作量统计数据。\n\n" +
"日志内容包含多个项目,每个项目之间用 === 项目: xxx === 标识。\n" +
"工作周期: " + period + "\n\n" +
"SVN日志内容:\n" + markdownContent + "\n\n" +
"请按照以下JSON格式返回工作量统计数据:\n" +
"{\n" +
" \"team\": \"所属班组\",\n" +
" \"contact\": \"技术对接人\",\n" +
" \"developer\": \"开发人员\",\n" +
" \"period\": \"" + period + "\",\n" +
" \"records\": [\n" +
" {\n" +
" \"sequence\": 1,\n" +
" \"project\": \"项目1/项目2/项目3\",\n" +
" \"content\": \"# 项目1\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目2\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目3\\n1.工作内容1\\n2.工作内容2\"\n" +
" }\n" +
" ]\n" +
"}\n\n" +
"重要要求:\n" +
"1. 根据日志作者确定开发人员\n" +
"2. 将所有项目的工作内容合并到一条记录中\n" +
"3. 项目名称字段project使用 / 分隔多个项目,例如:\"PRS7050场站系统/PRS7950智能巡视现场问题/PRS7950电科院测试\"\n" +
"4. 具体工作内容字段content使用 # 作为项目分类标识,格式为:\"# 项目名称\\n1.工作内容\\n2.工作内容\\n\\n# 下一个项目\\n1.工作内容\"\n" +
"5. 不同项目之间用空行分隔\n" +
"6. 只返回JSON不要有其他文字\n" +
"7. 提取具体工作内容,要详细和有条理\n" +
"8. 项目名称要简洁明确,去掉多余的前缀和后缀";
}
/**
* 调用DeepSeek API流式输出
*/
private static String callDeepSeekAPI(String apiKey, String prompt) throws IOException {
JSONObject requestBody = new JSONObject();
requestBody.put("model", "deepseek-chat");
// 创建消息对象,包含 role 和 content 字段
JSONObject messageObj = new JSONObject();
messageObj.put("role", "user");
messageObj.put("content", prompt);
// 创建消息数组
com.google.gson.JsonArray messagesArray = new com.google.gson.JsonArray();
messagesArray.add(messageObj.jsonObject);
requestBody.put("messages", messagesArray);
requestBody.put("temperature", 0.7);
requestBody.put("max_tokens", 4000);
requestBody.put("stream", Optional.of(true)); // 启用流式输出
Request request = new Request.Builder()
.url(DEEPSEEK_API_URL)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(requestBody.toString(), MediaType.parse("application/json")))
.build();
StringBuilder fullResponse = new StringBuilder();
int chunkCount = 0;
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.err.println("API调用失败: " + response.code() + " " + response.message());
String errorResponse = response.body().string();
System.err.println("响应: " + errorResponse);
return null;
}
// 读取流式响应
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ")) {
String data = line.substring(6);
if (data.equals("[DONE]")) {
break;
}
try {
JSONObject chunk = new JSONObject(data);
if (chunk.has("choices") && chunk.getJSONArray("choices").size() > 0) {
JSONObject choice = chunk.getJSONArray("choices").get(0);
if (choice.has("delta")) {
JSONObject delta = choice.getJSONObject("delta");
if (delta.has("content")) {
String content = delta.optString("content", "");
fullResponse.append(content);
chunkCount++;
// 实时打印处理进度
System.out.print(content);
System.out.flush();
}
}
}
} catch (Exception e) {
// 忽略解析错误,继续处理下一行
}
}
}
}
} catch (Exception e) {
System.err.println("API调用过程中发生异常: " + e.getMessage());
e.printStackTrace();
return null;
}
System.out.println(); // 换行
System.out.println("收到 " + chunkCount + " 个数据块");
if (fullResponse.length() == 0) {
System.err.println("警告: 未收到任何响应内容");
}
return fullResponse.toString();
}
/**
* 从响应中提取纯 JSON 内容
*/
private static String extractJson(String response) {
String trimmed = response.trim();
// 去除 ```json 标记
if (trimmed.startsWith("```json")) {
trimmed = trimmed.substring(7);
} else if (trimmed.startsWith("```")) {
trimmed = trimmed.substring(3);
}
// 去除 ``` 结束标记
if (trimmed.endsWith("```")) {
trimmed = trimmed.substring(0, trimmed.length() - 3);
}
return trimmed.trim();
}
/**
* 生成Excel文件
*/
private static void generateExcel(String outputPath, String jsonResponse) throws IOException {
// 提取纯 JSON 内容(去除 ```json 和 ``` 标记)
String cleanJson = extractJson(jsonResponse);
// 解析JSON响应
JSONObject data = new JSONObject(cleanJson);
// 创建工作簿
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("工作表1");
// 创建样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle contentStyle = createContentStyle(workbook);
CellStyle workContentStyle = createWorkContentStyle(workbook);
// 创建表头7列与参考文件一致
Row headerRow = sheet.createRow(0);
headerRow.setHeightInPoints(14.25f); // 表头行高
String[] headers = {"序号", "所属班组", "技术对接", "开发人员", "工作周期", "开发项目名称", "具体工作内容"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
// 设置固定列宽(与参考文件一致)
sheet.setColumnWidth(0, 2048); // 序号8.00字符
sheet.setColumnWidth(1, 3328); // 所属班组13.00字符
sheet.setColumnWidth(2, 4608); // 技术对接18.00字符
sheet.setColumnWidth(3, 3840); // 开发人员15.00字符
sheet.setColumnWidth(4, 5888); // 工作周期23.00字符
sheet.setColumnWidth(5, 14080); // 开发项目名称55.00字符
sheet.setColumnWidth(6, 43991); // 具体工作内容171.84字符
// 获取记录
String team = data.optString("team", "");
String contact = data.optString("contact", "");
String developer = data.optString("developer", "");
String period = data.optString("period", "");
if (data.has("records")) {
JSONArray recordsArray = data.getJSONArray("records");
int rowNum = 1;
for (int i = 0; i < recordsArray.size(); i++) {
JSONObject record = recordsArray.get(i);
Row row = sheet.createRow(rowNum++);
row.setHeightInPoints(16.50f); // 内容行高
// 序号
Cell cell0 = row.createCell(0);
cell0.setCellValue(record.optDouble("sequence", i + 1));
cell0.setCellStyle(contentStyle);
// 所属班组
Cell cell1 = row.createCell(1);
cell1.setCellValue(team);
cell1.setCellStyle(contentStyle);
// 技术对接
Cell cell2 = row.createCell(2);
cell2.setCellValue(contact);
cell2.setCellStyle(contentStyle);
// 开发人员
Cell cell3 = row.createCell(3);
cell3.setCellValue(developer);
cell3.setCellStyle(contentStyle);
// 工作周期
Cell cell4 = row.createCell(4);
cell4.setCellValue(period);
cell4.setCellStyle(contentStyle);
// 项目名称(多个项目用 / 分隔)
Cell cell5 = row.createCell(5);
cell5.setCellValue(record.optString("project", ""));
cell5.setCellStyle(contentStyle);
// 工作内容(支持换行,用 # 标识不同项目)
Cell cell6 = row.createCell(6);
cell6.setCellValue(record.optString("content", ""));
cell6.setCellStyle(workContentStyle); // 使用工作内容样式
}
}
// 写入文件
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
workbook.write(fos);
}
workbook.close();
}
/**
* 创建表头样式
*/
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("SimSun"); // 字体名称SimSun
font.setFontHeightInPoints((short) 11); // 字体大小11磅
font.setBold(false); // 不粗体
font.setColor(IndexedColors.BLACK.getIndex()); // 黑色
style.setFont(font);
style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中
style.setFillPattern(FillPatternType.NO_FILL); // 无填充
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
style.setRightBorderColor(IndexedColors.BLACK.getIndex());
style.setWrapText(false); // 不换行
return style;
}
/**
* 创建普通内容样式列A-F
*/
private static CellStyle createContentStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("宋体"); // 字体名称:宋体
font.setFontHeightInPoints((short) 11); // 字体大小11磅
font.setBold(false); // 不粗体
style.setFont(font);
style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中
style.setFillPattern(FillPatternType.NO_FILL); // 无填充
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.NONE);
style.setBorderLeft(BorderStyle.NONE);
style.setBorderRight(BorderStyle.NONE);
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
style.setWrapText(false); // 不换行
return style;
}
/**
* 创建工作内容样式列G
*/
private static CellStyle createWorkContentStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("NSimSun"); // 字体名称:新宋体
font.setFontHeightInPoints((short) 14); // 字体大小14磅
font.setBold(true); // 粗体
font.setColor(IndexedColors.BLACK.getIndex()); // 黑色
style.setFont(font);
style.setAlignment(HorizontalAlignment.LEFT); // 水平对齐:左对齐
style.setVerticalAlignment(VerticalAlignment.TOP); // 垂直对齐:顶部
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); // 黄色背景
style.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 实心填充
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.NONE);
style.setBorderLeft(BorderStyle.NONE);
style.setBorderRight(BorderStyle.NONE);
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
style.setWrapText(true); // 自动换行
return style;
}
/**
* 简单的JSON工具类
*/
static class JSONObject {
private final com.google.gson.JsonObject jsonObject;
public JSONObject() {
this.jsonObject = new com.google.gson.JsonObject();
}
public JSONObject(String jsonString) {
com.google.gson.Gson gson = new com.google.gson.Gson();
this.jsonObject = gson.fromJson(jsonString, com.google.gson.JsonObject.class);
}
public JSONObject(String key, String value) {
this();
put(key, value);
}
public void put(String key, String value) {
jsonObject.addProperty(key, value);
}
public void put(String key, int value) {
jsonObject.addProperty(key, String.valueOf(value));
}
public void put(String key, double value) {
jsonObject.addProperty(key, String.valueOf(value));
}
public void put(String key, Object value) {
com.google.gson.Gson gson = new com.google.gson.Gson();
jsonObject.add(key, gson.toJsonTree(value));
}
public String optString(String key, String defaultValue) {
if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) {
return jsonObject.get(key).getAsString();
}
return defaultValue;
}
public double optDouble(String key, double defaultValue) {
if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) {
return jsonObject.get(key).getAsDouble();
}
return defaultValue;
}
public boolean has(String key) {
return jsonObject.has(key);
}
public JSONArray getJSONArray(String key) {
return new JSONArray(jsonObject.get(key).getAsJsonArray());
}
public JSONObject getJSONObject(String key) {
return new JSONObject(jsonObject.get(key).getAsJsonObject().toString());
}
@Override
public String toString() {
return jsonObject.toString();
}
}
/**
* 简单的JSONArray工具类
*/
static class JSONArray {
private final com.google.gson.JsonArray jsonArray;
public JSONArray(com.google.gson.JsonArray jsonArray) {
this.jsonArray = jsonArray;
}
public int size() {
return jsonArray.size();
}
public JSONObject get(int index) {
return new JSONObject(jsonArray.get(index).getAsJsonObject().toString());
}
@SuppressWarnings("unchecked")
public <T> java.util.List<JSONObject> toList() {
java.util.List<JSONObject> list = new ArrayList<>();
for (int i = 0; i < jsonArray.size(); i++) {
list.add(get(i));
}
return list;
}
}
}

View File

@@ -0,0 +1,84 @@
package com.svnlog;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 临时工具类用于分析现有Excel文件格式
*/
public class ExcelAnalyzer {
public static void main(String[] args) {
String excelPath = "/home/liumangmang/opencode/日志/202512工作量统计_刘靖.xlsx";
try (FileInputStream fis = new FileInputStream(excelPath);
Workbook workbook = new XSSFWorkbook(fis)) {
System.out.println("工作表数量: " + workbook.getNumberOfSheets());
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
System.out.println("工作表 " + i + ": " + workbook.getSheetName(i));
}
Sheet sheet = workbook.getSheetAt(0);
System.out.println("\n工作表名称: " + sheet.getSheetName());
System.out.println("总行数: " + sheet.getPhysicalNumberOfRows());
System.out.println("最后一行索引: " + sheet.getLastRowNum());
// 读取前20行数据
System.out.println("\n前20行数据:");
for (int i = 0; i <= Math.min(19, sheet.getLastRowNum()); i++) {
Row row = sheet.getRow(i);
if (row != null) {
System.out.print("" + (i + 1) + "行: ");
for (Cell cell : row) {
String value = getCellValueAsString(cell);
System.out.print("[" + value + "] ");
}
System.out.println();
}
}
// 读取表头
Row headerRow = sheet.getRow(0);
if (headerRow != null) {
System.out.println("\n表头列数: " + headerRow.getLastCellNum());
System.out.print("表头: ");
for (Cell cell : headerRow) {
System.out.print("[" + getCellValueAsString(cell) + "] ");
}
System.out.println();
}
} catch (IOException e) {
System.err.println("读取Excel文件出错: " + e.getMessage());
e.printStackTrace();
}
}
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
case BLANK:
return "";
default:
return "";
}
}
}

View File

@@ -0,0 +1,71 @@
package com.svnlog;
import java.util.Date;
public class LogEntry {
private long revision;
private String author;
private Date date;
private String message;
private String[] changedPaths;
public LogEntry() {
}
public LogEntry(long revision, String author, Date date, String message) {
this.revision = revision;
this.author = author;
this.date = date;
this.message = message;
}
public long getRevision() {
return revision;
}
public void setRevision(long revision) {
this.revision = revision;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String[] getChangedPaths() {
return changedPaths;
}
public void setChangedPaths(String[] changedPaths) {
this.changedPaths = changedPaths;
}
@Override
public String toString() {
return "LogEntry{" +
"revision=" + revision +
", author='" + author + '\'' +
", date=" + date +
", message='" + message + '\'' +
'}';
}
}

View File

@@ -0,0 +1,215 @@
package com.svnlog;
import org.tmatesoft.svn.core.SVNException;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class Main {
private static final Scanner scanner = new Scanner(System.in);
private static final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
// 预设项目列表
private static final Project[] PRESET_PROJECTS = {
new Project("PRS-7050场站智慧管控", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_java"),
new Project("PRS-7950在线巡视", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java"),
new Project("PRS-7950在线巡视电科院测试版", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java")
};
public static void main(String[] args) {
System.out.println("===========================================");
System.out.println(" SVN 日志查询工具 v1.0");
System.out.println("===========================================");
System.out.println();
try {
// 创建 md 目录
File mdDir = new File("md");
if (!mdDir.exists()) {
boolean created = mdDir.mkdir();
if (created) {
System.out.println("已创建 md 目录用于存放日志文件");
}
}
System.out.println();
// 选择项目
Project selectedProject = selectProject();
String url = selectedProject.getUrl();
System.out.println("已选择项目: " + selectedProject.getName());
System.out.println("SVN地址: " + url);
System.out.println();
String username = readInput("请输入SVN账号: ");
String password = readPassword("请输入SVN密码: ");
System.out.println("正在连接SVN仓库...");
SVNLogFetcher fetcher = new SVNLogFetcher(url, username, password);
fetcher.testConnection();
System.out.println("连接成功!");
System.out.println();
long latestRevision = fetcher.getLatestRevision();
System.out.println("最新版本号: " + latestRevision);
System.out.println();
long startRevision = readLongInput("请输入开始版本号 (回车使用最新版本): ", latestRevision);
long endRevision = readLongInput("请输入结束版本号 (回车使用最新版本): ", latestRevision);
String filterUser = readInput("请输入过滤用户名 (包含匹配,回车跳过过滤): ");
System.out.println();
System.out.println("正在获取日志...");
List<LogEntry> logs = fetcher.fetchLogs(startRevision, endRevision, filterUser);
if (logs.isEmpty()) {
System.out.println("没有找到符合条件的日志记录。");
return;
}
System.out.println("获取到 " + logs.size() + " 条日志记录。");
System.out.println();
// 生成Markdown文件保存到 md 目录)
String fileName = "md/svn_log_" + selectedProject.getName() + "_" + fileNameDateFormat.format(new Date()) + ".md";
generateMarkdown(fileName, url, username, startRevision, endRevision, filterUser, logs, fetcher);
System.out.println();
System.out.println("日志已成功导出到: " + fileName);
System.out.println();
} catch (SVNException e) {
System.err.println("SVN错误: " + e.getMessage());
} catch (Exception e) {
System.err.println("发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 让用户选择项目
*/
private static Project selectProject() {
System.out.println("请选择SVN项目:");
for (int i = 0; i < PRESET_PROJECTS.length; i++) {
System.out.println(" " + (i + 1) + ". " + PRESET_PROJECTS[i].getName());
}
System.out.println(" 0. 自定义SVN地址");
System.out.println();
while (true) {
System.out.print("请输入项目编号 (1-" + PRESET_PROJECTS.length + ", 0为自定义): ");
String input = scanner.nextLine().trim();
try {
int choice = Integer.parseInt(input);
if (choice == 0) {
String customUrl = readInput("请输入SVN仓库地址: ");
return new Project("自定义项目", customUrl);
} else if (choice >= 1 && choice <= PRESET_PROJECTS.length) {
return PRESET_PROJECTS[choice - 1];
} else {
System.out.println("输入无效,请重新选择!");
}
} catch (NumberFormatException e) {
System.out.println("输入无效,请输入数字!");
}
}
}
private static String readInput(String prompt) {
System.out.print(prompt);
return scanner.nextLine().trim();
}
private static String readPassword(String prompt) {
if (System.console() != null) {
char[] password = System.console().readPassword("%s", prompt);
return new String(password);
} else {
System.out.print(prompt);
return scanner.nextLine();
}
}
private static long readLongInput(String prompt, long defaultValue) {
System.out.print(prompt);
String input = scanner.nextLine().trim();
if (input.isEmpty()) {
return defaultValue;
}
try {
return Long.parseLong(input);
} catch (NumberFormatException e) {
System.out.println("输入无效,使用默认值: " + defaultValue);
return defaultValue;
}
}
private static void generateMarkdown(String fileName, String url, String username,
long startRevision, long endRevision, String filterUser,
List<LogEntry> logs, SVNLogFetcher fetcher) throws IOException {
StringBuilder markdown = new StringBuilder();
// 标题
markdown.append("# SVN 日志报告\n\n");
// 查询条件(简化版)
markdown.append("## 查询条件\n\n");
markdown.append("- **SVN地址**: `").append(url).append("`\n");
markdown.append("- **版本范围**: r").append(startRevision).append(" - r").append(endRevision).append("\n");
if (filterUser != null && !filterUser.isEmpty()) {
markdown.append("- **过滤用户**: `").append(filterUser).append("`\n");
}
markdown.append("\n");
// 日志详情(简化版,只包含作者、时间、版本、提交信息)
markdown.append("## 日志详情\n\n");
for (LogEntry entry : logs) {
markdown.append("### r").append(entry.getRevision()).append("\n\n");
markdown.append("**作者**: `").append(entry.getAuthor()).append("` \n");
markdown.append("**时间**: ").append(fetcher.formatDate(entry.getDate())).append(" \n");
markdown.append("**版本**: r").append(entry.getRevision()).append("\n\n");
String message = entry.getMessage();
if (message != null && !message.isEmpty()) {
markdown.append("**提交信息**:\n\n");
markdown.append("```\n").append(message).append("\n```\n\n");
} else {
markdown.append("**提交信息**: (无)\n\n");
}
markdown.append("---\n\n");
}
// 写入文件
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write(markdown.toString());
}
}
/**
* 项目信息类
*/
private static class Project {
private String name;
private String url;
public Project(String name, String url) {
this.name = name;
this.url = url;
}
public String getName() {
return name;
}
public String getUrl() {
return url;
}
}
}

View File

@@ -0,0 +1,98 @@
package com.svnlog;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import java.text.SimpleDateFormat;
import java.util.*;
public class SVNLogFetcher {
private String url;
private String username;
private String password;
private SVNRepository repository;
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public SVNLogFetcher(String url, String username, String password) throws SVNException {
this.url = url;
this.username = username;
this.password = password;
SVNRepositoryFactoryImpl.setup();
this.repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(url));
ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(username, password.toCharArray());
repository.setAuthenticationManager(authManager);
}
public List<LogEntry> fetchLogs(long startRevision, long endRevision) throws SVNException {
return fetchLogs(startRevision, endRevision, null);
}
public List<LogEntry> fetchLogs(long startRevision, long endRevision, String filterUser) throws SVNException {
List<LogEntry> entries = new ArrayList<>();
if (startRevision < 0) {
startRevision = repository.getLatestRevision();
}
if (endRevision < 0) {
endRevision = repository.getLatestRevision();
}
if (startRevision > endRevision) {
long temp = startRevision;
startRevision = endRevision;
endRevision = temp;
}
Collection<SVNLogEntry> logEntries = repository.log(new String[]{""}, null, startRevision, endRevision, true, true);
for (SVNLogEntry logEntry : logEntries) {
String author = logEntry.getAuthor();
// 如果设置了用户名过滤器,则跳过不匹配的记录(包含匹配,不区分大小写)
if (filterUser != null && !filterUser.isEmpty() && (author == null || !author.toLowerCase().contains(filterUser.toLowerCase()))) {
continue;
}
LogEntry entry = new LogEntry();
entry.setRevision(logEntry.getRevision());
entry.setAuthor(author != null ? author : "(无作者)");
entry.setDate(logEntry.getDate());
entry.setMessage(logEntry.getMessage() != null ? logEntry.getMessage().trim() : "");
// 获取变更的文件路径
if (logEntry.getChangedPaths() != null) {
List<String> paths = new ArrayList<>();
for (Map.Entry<String, SVNLogEntryPath> pathEntry : logEntry.getChangedPaths().entrySet()) {
paths.add(pathEntry.getKey());
}
entry.setChangedPaths(paths.toArray(new String[0]));
}
entries.add(entry);
}
// 按版本号降序排序
entries.sort((e1, e2) -> Long.compare(e2.getRevision(), e1.getRevision()));
return entries;
}
public long getLatestRevision() throws SVNException {
return repository.getLatestRevision();
}
public String formatDate(Date date) {
return dateFormat.format(date);
}
public void testConnection() throws SVNException {
repository.testConnection();
}
}