first commit

This commit is contained in:
2026-06-09 17:51:03 +08:00
commit ebf30399fb
47 changed files with 5532 additions and 0 deletions

View 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 配对为 RENAMEDGit 未显式标记 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;
}
}
}
}