first commit
This commit is contained in:
295
src/main/java/com/codechecker/git/GitChangeScanner.java
Normal file
295
src/main/java/com/codechecker/git/GitChangeScanner.java
Normal file
@@ -0,0 +1,295 @@
|
||||
package com.codechecker.git;
|
||||
|
||||
import com.codechecker.model.ChangedClassFile;
|
||||
import com.codechecker.model.ClassType;
|
||||
import com.codechecker.parser.ClassDeclParser;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 执行 git diff,识别 Dto/Vo/Entity/Model 的修改、删除、重命名(含 R* 与同目录 D+A 配对)。
|
||||
*/
|
||||
public class GitChangeScanner {
|
||||
private final Path repoRoot;
|
||||
private final ClassDeclParser classDeclParser = new ClassDeclParser();
|
||||
|
||||
public GitChangeScanner(Path repoRoot) {
|
||||
this.repoRoot = repoRoot;
|
||||
}
|
||||
|
||||
/** 扫描两次提交间的模型类变更 */
|
||||
public List<ChangedClassFile> scanChangedClasses(String oldSha, String newSha) throws IOException {
|
||||
List<String> lines = runGit("diff", "--name-status", oldSha, newSha);
|
||||
List<ChangedClassFile> deletions = new ArrayList<>();
|
||||
Map<String, PendingAdd> additionsByParent = new LinkedHashMap<>();
|
||||
List<ChangedClassFile> result = new ArrayList<>();
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = line.split("\t");
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
String status = parts[0].trim();
|
||||
|
||||
if (status.startsWith("R") && parts.length >= 3) {
|
||||
ChangedClassFile renamed = buildRenamed(parts[1], parts[2], oldSha, newSha);
|
||||
if (renamed != null) {
|
||||
result.add(renamed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String path = normalizePath(parts[parts.length - 1]);
|
||||
if (!path.endsWith(".java")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String fallbackName = ClassDeclParser.classNameFromPath(path);
|
||||
ClassType classType = ClassType.fromClassName(fallbackName);
|
||||
if (classType == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.equals("A")) {
|
||||
String newSource = readFileAtCommit(newSha, path);
|
||||
String className = classDeclParser.resolveClassName(newSource, fallbackName);
|
||||
additionsByParent.computeIfAbsent(parentDir(path), k -> new PendingAdd())
|
||||
.add(path, className, classType, newSource);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.equals("D")) {
|
||||
String oldSource = readFileAtCommit(oldSha, path);
|
||||
String className = classDeclParser.resolveClassName(oldSource, fallbackName);
|
||||
deletions.add(new ChangedClassFile(path, ChangedClassFile.ChangeStatus.DELETED, className, classType));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.startsWith("M")) {
|
||||
ChangedClassFile modified = buildModified(path, oldSha, newSha, fallbackName, classType);
|
||||
if (modified != null) {
|
||||
result.add(modified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pairDeleteAndAdd(deletions, additionsByParent, oldSha, result);
|
||||
result.addAll(deletions);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 处理 git R 状态:路径重命名 */
|
||||
private ChangedClassFile buildRenamed(String oldPathRaw, String newPathRaw,
|
||||
String oldSha, String newSha) throws IOException {
|
||||
String oldPath = normalizePath(oldPathRaw);
|
||||
String newPath = normalizePath(newPathRaw);
|
||||
if (!oldPath.endsWith(".java") || !newPath.endsWith(".java")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String oldFallback = ClassDeclParser.classNameFromPath(oldPath);
|
||||
String newFallback = ClassDeclParser.classNameFromPath(newPath);
|
||||
ClassType classType = ClassType.fromClassName(newFallback);
|
||||
if (classType == null) {
|
||||
classType = ClassType.fromClassName(oldFallback);
|
||||
}
|
||||
if (classType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String oldSource = readFileAtCommit(oldSha, oldPath);
|
||||
String newSource = readFileAtCommit(newSha, newPath);
|
||||
String oldClassName = classDeclParser.resolveClassName(oldSource, oldFallback);
|
||||
String newClassName = classDeclParser.resolveClassName(newSource, newFallback);
|
||||
|
||||
return new ChangedClassFile(newPath, oldPath, ChangedClassFile.ChangeStatus.RENAMED,
|
||||
newClassName, oldClassName, classType);
|
||||
}
|
||||
|
||||
/** 处理 M 状态:同路径下对比 AST 类名判断是否重命名 */
|
||||
private ChangedClassFile buildModified(String path, String oldSha, String newSha,
|
||||
String fallbackName, ClassType classType) throws IOException {
|
||||
String oldSource = readFileAtCommit(oldSha, path);
|
||||
String newSource = readFileAtCommit(newSha, path);
|
||||
if (newSource == null || newSource.isBlank()) {
|
||||
newSource = readFileAtHead(path);
|
||||
}
|
||||
String oldClassName = classDeclParser.resolveClassName(oldSource, fallbackName);
|
||||
String newClassName = classDeclParser.resolveClassName(newSource, fallbackName);
|
||||
|
||||
if (oldClassName.equals(newClassName)) {
|
||||
return new ChangedClassFile(path, ChangedClassFile.ChangeStatus.MODIFIED,
|
||||
newClassName, classType);
|
||||
}
|
||||
return new ChangedClassFile(path, path, ChangedClassFile.ChangeStatus.RENAMED,
|
||||
newClassName, oldClassName, classType);
|
||||
}
|
||||
|
||||
/** 同目录 D+A 配对为 RENAMED(Git 未显式标记 R 时) */
|
||||
private void pairDeleteAndAdd(List<ChangedClassFile> deletions,
|
||||
Map<String, PendingAdd> additionsByParent,
|
||||
String oldSha,
|
||||
List<ChangedClassFile> result) throws IOException {
|
||||
List<ChangedClassFile> unpaired = new ArrayList<>();
|
||||
|
||||
for (ChangedClassFile deleted : deletions) {
|
||||
String parent = parentDir(deleted.getRelativePath());
|
||||
PendingAdd pending = additionsByParent.get(parent);
|
||||
if (pending == null || pending.isEmpty()) {
|
||||
unpaired.add(deleted);
|
||||
continue;
|
||||
}
|
||||
|
||||
PendingAdd.Candidate candidate = pending.poll(deleted.getClassType());
|
||||
if (candidate == null) {
|
||||
unpaired.add(deleted);
|
||||
continue;
|
||||
}
|
||||
|
||||
String oldSource = readFileAtCommit(oldSha, deleted.getRelativePath());
|
||||
String oldClassName = classDeclParser.resolveClassName(oldSource, deleted.getClassName());
|
||||
result.add(new ChangedClassFile(candidate.path(), deleted.getRelativePath(),
|
||||
ChangedClassFile.ChangeStatus.RENAMED,
|
||||
candidate.className(), oldClassName, deleted.getClassType()));
|
||||
}
|
||||
|
||||
deletions.clear();
|
||||
deletions.addAll(unpaired);
|
||||
}
|
||||
|
||||
/** 取路径父目录,用于 D+A 配对 */
|
||||
private static String parentDir(String path) {
|
||||
int idx = path.lastIndexOf('/');
|
||||
return idx >= 0 ? path.substring(0, idx) : "";
|
||||
}
|
||||
|
||||
/** 读取指定 commit 下的文件内容 */
|
||||
public String readFileAtCommit(String commitSha, String relativePath) throws IOException {
|
||||
List<String> lines = runGit("show", commitSha + ":" + relativePath);
|
||||
if (lines.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (lines.size() == 1 && lines.get(0).startsWith("fatal:")) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
/** 读取工作区 HEAD 文件(commit 中缺失时的回退) */
|
||||
public String readFileAtHead(String relativePath) throws IOException {
|
||||
Path file = repoRoot.resolve(relativePath);
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
}
|
||||
return Files.readString(file, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** 两次提交间变更文件路径列表(--name-only) */
|
||||
public List<String> diffNameOnly(String oldSha, String newSha) throws IOException {
|
||||
return runGit("diff", "--name-only", oldSha, newSha);
|
||||
}
|
||||
|
||||
/** 在 repoRoot 下执行 git 命令并返回 stdout 行 */
|
||||
private List<String> runGit(String... args) throws IOException {
|
||||
String[] command = new String[args.length + 3];
|
||||
command[0] = "git";
|
||||
command[1] = "-C";
|
||||
command[2] = repoRoot.toString();
|
||||
System.arraycopy(args, 0, command, 3, args.length);
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(command);
|
||||
builder.redirectErrorStream(true);
|
||||
Process process = builder.start();
|
||||
|
||||
List<String> output = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0 && !isBenignGitShowFailure(args, output)) {
|
||||
throw new IOException("git 命令失败: " + String.join(" ", command)
|
||||
+ "\n" + String.join("\n", output));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("git 命令被中断", e);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/** git show 文件不存在等情况视为可忽略 */
|
||||
private boolean isBenignGitShowFailure(String[] args, List<String> output) {
|
||||
if (args.length > 0 && "show".equals(args[0])) {
|
||||
String joined = String.join("\n", output).toLowerCase(Locale.ROOT);
|
||||
return joined.contains("exists on disk") || joined.contains("bad object")
|
||||
|| joined.contains("path") && joined.contains("does not exist");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 统一路径分隔符为 / */
|
||||
private String normalizePath(String path) {
|
||||
return path.replace("\\", "/");
|
||||
}
|
||||
|
||||
/** 同目录新增文件缓冲,供 D+A 配对 */
|
||||
private static final class PendingAdd {
|
||||
private final Map<ClassType, List<Candidate>> byType = new HashMap<>();
|
||||
|
||||
void add(String path, String className, ClassType classType, String source) {
|
||||
byType.computeIfAbsent(classType, k -> new ArrayList<>())
|
||||
.add(new Candidate(path, className));
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return byType.values().stream().allMatch(List::isEmpty);
|
||||
}
|
||||
|
||||
/** 按类型取出一个候选新增文件 */
|
||||
Candidate poll(ClassType classType) {
|
||||
List<Candidate> list = byType.get(classType);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return list.remove(0);
|
||||
}
|
||||
|
||||
private static final class Candidate {
|
||||
private final String path;
|
||||
private final String className;
|
||||
|
||||
private Candidate(String path, String className) {
|
||||
this.path = path;
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
private String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
private String className() {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user