From c40e4eff61e46e567380d561ed839054ef03585f Mon Sep 17 00:00:00 2001 From: dongzi Date: Mon, 3 Aug 2026 17:22:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20V1.1=20-=20=E8=B4=A3=E4=BB=BB=E4=BA=BA?= =?UTF-8?q?=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/serialization-schema-check.yaml | 3 + .idea/compiler.xml | 2 + docs/CI集成说明.md | 36 ++++++++-- .../cache/analyze/SchemaCheckAnalyzer.java | 45 +++++++++++-- .../cli/SerializationSchemaCheckerMain.java | 13 +++- .../codechecker/cache/git/GitDiffScanner.java | 55 ++++++++++++++++ .../codechecker/cache/report/CheckReport.java | 2 +- .../git/GitDiffScannerAuthorFilterTest.java | 66 +++++++++++++++++++ 8 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java diff --git a/.gitea/workflows/serialization-schema-check.yaml b/.gitea/workflows/serialization-schema-check.yaml index ac0d288..8c00092 100644 --- a/.gitea/workflows/serialization-schema-check.yaml +++ b/.gitea/workflows/serialization-schema-check.yaml @@ -199,6 +199,8 @@ jobs: COMMIT_TIME=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S' "${NEW_SHA}") + # 对比区间仍为 before→after 全量;--responsible-author 只把推送人非 merge 触及的文件作触发集 + # (他人改动应已在其各自 push 时告警;纯 merge 推送 → 空触发集 → 不告警) java -jar "/tmp/serialization-schema-checker-${SERIALIZATION_SCHEMA_CHECKER_VERSION}.jar" \ --config .gitea/config/serialization-schema-check-config.yaml \ --repo-root . \ @@ -206,4 +208,5 @@ jobs: --new-sha "$NEW_SHA" \ --branch "${{ gitea.ref_name }}" \ --modifier "${{ gitea.actor }}" \ + --responsible-author "${{ gitea.actor }}" \ --modify-time "$COMMIT_TIME" diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b94671e..16d10db 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,6 +2,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/docs/CI集成说明.md b/docs/CI集成说明.md index b2234ac..5eab126 100644 --- a/docs/CI集成说明.md +++ b/docs/CI集成说明.md @@ -29,7 +29,7 @@ mode=block 且含任意结构变更 → exit 1(流水线失败) 一次 push 推了多个 commit 时,只跑 **一次** 检测,覆盖整次 push 的累计代码差,**不会**因为「变更发生在中间 commit、最后一个 commit 没改相关文件」而漏检。 -不需要全量历史:工作树是当前 tip;`before` 通过 `git fetch --depth 1 `(或 deepen)取到即可。 +不需要全量历史:工作树是当前 tip;`before` 通过 `git fetch --depth 1 `(或 deepen)取到即可。责任人过滤依赖 `git log old..new`,流水线已通过 `ensure_range` deepen,保证区间提交可见。 边界: @@ -38,6 +38,28 @@ mode=block 且含任意结构变更 → exit 1(流水线失败) - 中间 commit 改坏又被末 commit 改回 → 累计可能无告警(以最终结构为准) - commit 计数:优先取 `gitea.event.commits` 长度;否则 deepen 到 `before` 为祖先后再 `git rev-list`(浅克隆下直接 `rev-list` 会少算) +### 1.2 责任人过滤(合并推送不背锅) + +**前提**:A/B 各自 push 都会跑检测;B 的结构变更已在 B 的 push 告警。合并/代推时不应再让推送人背他人的锅,也不按人拆多条通知。 + +| 项 | 行为 | +|----|------| +| 对比区间 | 仍为整次 push 的 `before → after`(Schema 新旧用全量 diff 回填) | +| 触发集 | 仅 **责任人非 merge 提交** 触及的 Java 文件 ∩ 区间 diff | +| 责任人参数 | `--responsible-author`(流水线传 `gitea.actor`);`--modifier` 同为 actor,报告抬头「责任人」 | +| Author 匹配 | 对 `git log` 的 `%an <%ae>` **不区分大小写包含** actor(中文名/邮箱对不上时可能匹配失败) | +| `--no-merges` | 纯 merge 推送 → 触发集为空 → **不告警**(符合前提) | +| 责任人无自有 Java 改动 | 空报告、打日志跳过 | + +```bash +java -jar serialization-schema-checker-1.0.0.jar \ + --old-sha "$OLD_SHA" \ + --new-sha "$NEW_SHA" \ + --modifier "${{ gitea.actor }}" \ + --responsible-author "${{ gitea.actor }}" \ + ... +``` + --- ## 2. 前置条件 @@ -83,10 +105,12 @@ git checkout -B "$BRANCH" "$NEW_SHA" # NEW_SHA = gitea.sha git fetch --depth 1 origin "$OLD_SHA" # 优先 # 或 git fetch --deepen N # 兜底 -# 4) 执行 +# 4) 执行(对比区间全量;责任人过滤触发集) java -jar serialization-schema-checker-1.0.0.jar \ --old-sha "$OLD_SHA" \ --new-sha "$NEW_SHA" \ + --modifier "${{ gitea.actor }}" \ + --responsible-author "${{ gitea.actor }}" \ ... ``` @@ -146,6 +170,8 @@ com/codechecker/serialization-schema-checker/1.0.0/ | commit 数显示为 1(实际多个) | 浅克隆下 `rev-list` 看不到中间提交 | 已修复:优先事件 `commits` 长度;并 deepen 到 before 为祖先 | | 漏报(模式/模块) | W0x/MQ 未开 / include_modules 过窄 | 确认 W01~W05 与 mq_patterns(含 MQ03~05、MQ-K03/K04);检查 `mq_read_hints_enabled` 与模块过滤 | | 类型展开不完整 | 类型在依赖 jar 中 | 补充 `manual_mappings.value_type` | +| 合并推送仍告他人改动 | 未传 `--responsible-author` / jar 过旧 | 同步流水线模板;确认 jar ≥ 含责任人过滤的版本 | +| 责任人过滤后漏报自己的改动 | actor 与 `%an <%ae>` 对不上 | 看日志「责任人过滤…」;核对 commit author 是否含 actor 子串 | --- @@ -157,6 +183,7 @@ com/codechecker/serialization-schema-checker/1.0.0/ # OLD = 推送前 tip,NEW = 当前 tip(可用 origin/branch@{1} 或显式 sha) OLD_SHA=$(git rev-parse origin/$(git branch --show-current)~3) # 示例:假设 ahead 3 NEW_SHA=$(git rev-parse HEAD) +AUTHOR=$(git log -1 --format=%an) java -jar /path/to/serialization-schema-checker-1.0.0.jar \ --config .gitea/config/serialization-schema-check-config.yaml \ @@ -164,12 +191,13 @@ java -jar /path/to/serialization-schema-checker-1.0.0.jar \ --old-sha "$OLD_SHA" \ --new-sha "$NEW_SHA" \ --branch $(git branch --show-current) \ - --modifier "$(git log -1 --format=%an)" \ + --modifier "$AUTHOR" \ + --responsible-author "$AUTHOR" \ --modify-time "$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S')" \ --dry-run ``` -单 commit 自测仍可用 `--old-sha HEAD~1 --new-sha HEAD`。 +单 commit 自测仍可用 `--old-sha HEAD~1 --new-sha HEAD`。未传 `--responsible-author` 时不做作者过滤(全量 diff 触发)。 --- diff --git a/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java b/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java index 34a5475..4ca53c8 100644 --- a/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java +++ b/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java @@ -56,26 +56,61 @@ public class SchemaCheckAnalyzer { * @throws GitException git 命令失败时 */ public CheckReport analyze(String oldSha, String newSha) throws GitException { + return analyze(oldSha, newSha, null); + } + + /** + * 对比两个提交之间的序列化结构变更。 + * + * @param oldSha 基准提交 + * @param newSha 当前提交 + * @param responsibleAuthor 责任人过滤(如 Gitea actor);非空时仅将「该作者非 merge 提交触及的文件」 + * 作为检测触发集(他人已在各自 push 告警过的改动不再挂到合并推送人) + * @return 检测报告 + * @throws GitException git 命令失败时 + */ + public CheckReport analyze(String oldSha, String newSha, String responsibleAuthor) throws GitException { GitDiffScanner scanner = new GitDiffScanner(repoRoot); List changedRaw = scanner.changedJavaFiles(oldSha, newSha); - Set changedFiles = new LinkedHashSet<>(); + Set allDiffFiles = new LinkedHashSet<>(); FileScanner fileScanner = new FileScanner(repoRoot, config.getIncludeModules(), config.getExcludeModules()); for (String path : changedRaw) { if (path.contains("/src/main/java/") && fileScanner.moduleAllowed(path) && !isFileIgnored(path)) { - changedFiles.add(path); + allDiffFiles.add(path); + } + } + + Set changedFiles = allDiffFiles; + String authorFilter = responsibleAuthor == null ? null : responsibleAuthor.trim(); + if (authorFilter != null && !authorFilter.isEmpty()) { + Set authorFiles = scanner.javaFilesTouchedByAuthor(oldSha, newSha, authorFilter); + Set scoped = new LinkedHashSet<>(); + for (String path : allDiffFiles) { + if (authorFiles.contains(path)) { + scoped.add(path); + } + } + changedFiles = scoped; + System.out.println("[serialization-schema-checker] 责任人过滤: " + authorFilter + + ",区间 diff 文件 " + allDiffFiles.size() + + " → 责任人触及 " + changedFiles.size()); + if (changedFiles.isEmpty()) { + System.out.println("[serialization-schema-checker] 责任人无非 merge 自有 Java 改动," + + "跳过结构告警(相关变更应已在原作者 push 时通知)。"); + return buildReport(oldSha, newSha, Collections.emptyList(), Collections.emptyMap()); } } // 当前工作树(= newSha 检出)内容 Map newContents = fileScanner.scan(); - // 旧版本内容:以工作树为基线,用 old 版本覆盖变更文件 + // 旧版本内容:必须以「区间全部 diff 文件」回填,避免他人改动的类型在索引中新旧一致导致漏 diff Map oldContents = new LinkedHashMap<>(newContents); - for (String path : changedFiles) { + for (String path : allDiffFiles) { String oldContent = scanner.fileContentAt(oldSha, path); if (oldContent == null) { - oldContents.remove(path); // 新增文件在旧版本不存在 + oldContents.remove(path); } else { oldContents.put(path, oldContent); } diff --git a/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java b/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java index ddb8591..c744a10 100644 --- a/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java +++ b/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java @@ -40,7 +40,7 @@ public class SerializationSchemaCheckerMain implements Callable { @Option(names = "--branch", description = "分支名(用于报告展示)") private String branch; - @Option(names = "--modifier", description = "提交人") + @Option(names = "--modifier", description = "责任人/推送人(报告展示;建议与 --responsible-author 一致)") private String modifier; @Option(names = "--modify-time", description = "提交时间") @@ -52,6 +52,10 @@ public class SerializationSchemaCheckerMain implements Callable { @Option(names = "--dry-run", description = "只输出报告,不发送企微通知") private boolean dryRun; + @Option(names = "--responsible-author", + description = "责任人过滤:仅检测该作者非 merge 提交触及的 Java 文件(通常传 Gitea actor)") + private String responsibleAuthor; + @Override public Integer call() { try { @@ -69,10 +73,13 @@ public class SerializationSchemaCheckerMain implements Callable { Path root = repoRoot.toAbsolutePath().normalize(); SchemaCheckAnalyzer analyzer = new SchemaCheckAnalyzer(config, root); - CheckReport report = analyzer.analyze(oldSha, newSha); + String author = responsibleAuthor != null && !responsibleAuthor.trim().isEmpty() + ? responsibleAuthor.trim() + : (modifier != null ? modifier.trim() : null); + CheckReport report = analyzer.analyze(oldSha, newSha, author); report.setBranch(branch); - report.setModifier(modifier); + report.setModifier(modifier != null ? modifier : author); report.setModifyTime(modifyTime); report.setRepository(repository != null ? repository : root.getFileName().toString()); diff --git a/src/main/java/com/codechecker/cache/git/GitDiffScanner.java b/src/main/java/com/codechecker/cache/git/GitDiffScanner.java index 19672db..7c40654 100644 --- a/src/main/java/com/codechecker/cache/git/GitDiffScanner.java +++ b/src/main/java/com/codechecker/cache/git/GitDiffScanner.java @@ -8,7 +8,10 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; +import java.util.Set; /** * 基于 git 命令的差异扫描与双版本文件内容读取。 @@ -18,6 +21,9 @@ import java.util.List; */ public class GitDiffScanner { + /** {@code git log --pretty} 中标识 commit 头的前缀(解析责任人触及文件时用)。 */ + static final String COMMIT_HEADER_PREFIX = "COMMIT "; + private final Path repoRoot; public GitDiffScanner(Path repoRoot) { @@ -40,6 +46,55 @@ public class GitDiffScanner { return result; } + /** + * 返回 old..new 区间内、由责任人非 merge提交触及的 Java 文件。 + *

用于「作者 push 已告警、合并推送人不再背锅」:只把责任人自己的改动纳入检测触发集。

+ * + * @param authorFilter 责任人过滤串(通常为 Gitea actor);与 author name/email 做不区分大小写的包含匹配 + * @return 相对路径集合;无匹配时为空 + */ + public Set javaFilesTouchedByAuthor(String oldSha, String newSha, String authorFilter) + throws GitException { + if (authorFilter == null || authorFilter.trim().isEmpty()) { + return new LinkedHashSet<>(); + } + List lines = runLines( + "git", "log", "--no-merges", "--name-only", + "--pretty=format:" + COMMIT_HEADER_PREFIX + "%an <%ae>", + oldSha + ".." + newSha, "--", "*.java"); + return parseAuthorTouchedJavaFiles(lines, authorFilter.trim()); + } + + /** + * 解析 {@link #javaFilesTouchedByAuthor} 的 git log 输出。 + */ + static Set parseAuthorTouchedJavaFiles(List lines, String authorFilter) { + Set result = new LinkedHashSet<>(); + if (lines == null || authorFilter == null || authorFilter.isEmpty()) { + return result; + } + String needle = authorFilter.toLowerCase(Locale.ROOT); + boolean take = false; + for (String raw : lines) { + if (raw == null) { + continue; + } + String line = raw.trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith(COMMIT_HEADER_PREFIX)) { + String identity = line.substring(COMMIT_HEADER_PREFIX.length()).toLowerCase(Locale.ROOT); + take = identity.contains(needle); + continue; + } + if (take && line.endsWith(".java")) { + result.add(line.replace('\\', '/')); + } + } + return result; + } + /** * 读取某提交下指定文件的内容;文件在该提交不存在时返回 null。 */ diff --git a/src/main/java/com/codechecker/cache/report/CheckReport.java b/src/main/java/com/codechecker/cache/report/CheckReport.java index 072cc3d..93df5fa 100644 --- a/src/main/java/com/codechecker/cache/report/CheckReport.java +++ b/src/main/java/com/codechecker/cache/report/CheckReport.java @@ -22,7 +22,7 @@ public class CheckReport { private String oldSha; /** 当前提交 SHA */ private String newSha; - /** 提交人 */ + /** 责任人 / 推送人(报告抬头展示;流水线责任人过滤时通常为 Gitea actor) */ private String modifier; /** 提交时间(展示用字符串) */ private String modifyTime; diff --git a/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java b/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java new file mode 100644 index 0000000..c020c48 --- /dev/null +++ b/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java @@ -0,0 +1,66 @@ +package com.codechecker.cache.git; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link GitDiffScanner#parseAuthorTouchedJavaFiles} 责任人过滤解析单测。 + */ +class GitDiffScannerAuthorFilterTest { + + @Test + void takesOnlyMatchingAuthorJavaFiles() { + Set files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList( + "COMMIT Alice ", + "a/src/main/java/A.java", + "a/src/main/java/Shared.java", + "COMMIT Bob ", + "b/src/main/java/B.java", + "a/src/main/java/Shared.java" + ), "alice"); + + assertEquals(Set.of( + "a/src/main/java/A.java", + "a/src/main/java/Shared.java" + ), files); + } + + @Test + void matchesActorAgainstEmailCaseInsensitive() { + Set files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList( + "COMMIT 张三 ", + "pkg/Foo.java", + "COMMIT 李四 ", + "pkg/Bar.java" + ), "zhangsan"); + + assertEquals(Set.of("pkg/Foo.java"), files); + } + + @Test + void skipsNonJavaAndEmptyLines() { + Set files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList( + "COMMIT alice ", + "", + "README.md", + "src/Main.java", + "COMMIT other ", + "src/Other.java" + ), "alice"); + + assertEquals(Set.of("src/Main.java"), files); + } + + @Test + void emptyFilterOrLinesYieldEmpty() { + assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles(Collections.emptyList(), "alice").isEmpty()); + assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles( + Collections.singletonList("COMMIT alice "), "").isEmpty()); + } +}