package com.codechecker.cache.git; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; /** * 基于 git 命令的差异扫描与双版本文件内容读取。 * *

通过 {@code git diff --name-only} 获取变更的 .java 文件,通过 {@code git show sha:path} * 读取指定提交下的文件内容,避免检出两个完整 worktree。

*/ public class GitDiffScanner { private final Path repoRoot; public GitDiffScanner(Path repoRoot) { this.repoRoot = repoRoot; } /** * 返回 old..new 之间变更的 Java 文件路径(相对仓库根,使用 / 分隔)。 */ public List changedJavaFiles(String oldSha, String newSha) throws GitException { List lines = runLines( "git", "diff", "--name-only", "--diff-filter=ACMR", oldSha, newSha, "--", "*.java"); List result = new ArrayList<>(); for (String line : lines) { String trimmed = line.trim(); if (!trimmed.isEmpty() && trimmed.endsWith(".java")) { result.add(trimmed); } } return result; } /** * 读取某提交下指定文件的内容;文件在该提交不存在时返回 null。 */ public String fileContentAt(String sha, String path) throws GitException { try { ProcessResult pr = run("git", "show", sha + ":" + path); if (pr.exitCode != 0) { return null; } return pr.stdout; } catch (GitException e) { return null; } } private List runLines(String... command) throws GitException { ProcessResult pr = run(command); if (pr.exitCode != 0) { throw new GitException("git 命令执行失败(exit=" + pr.exitCode + "): " + String.join(" ", command) + "\n" + pr.stderr); } List lines = new ArrayList<>(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(new java.io.ByteArrayInputStream( pr.stdout.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { lines.add(line); } } catch (IOException e) { throw new GitException("读取 git 输出失败", e); } return lines; } private ProcessResult run(String... command) throws GitException { ProcessBuilder pb = new ProcessBuilder(command); pb.directory(repoRoot.toFile()); pb.redirectErrorStream(false); try { Process process = pb.start(); String stdout = readStream(process.getInputStream()); String stderr = readStream(process.getErrorStream()); int exit = process.waitFor(); return new ProcessResult(exit, stdout, stderr); } catch (IOException e) { throw new GitException("无法启动 git 进程: " + String.join(" ", command), e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new GitException("git 进程被中断", e); } } private static String readStream(InputStream in) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[8192]; int read; while ((read = in.read(chunk)) != -1) { buffer.write(chunk, 0, read); } return new String(buffer.toByteArray(), StandardCharsets.UTF_8); } private static final class ProcessResult { final int exitCode; final String stdout; final String stderr; ProcessResult(int exitCode, String stdout, String stderr) { this.exitCode = exitCode; this.stdout = stdout; this.stderr = stderr; } } }