diff --git a/.gitea/workflows/serialization-schema-check.yaml b/.gitea/workflows/serialization-schema-check.yaml index 12cf6ed..f0eaa9e 100644 --- a/.gitea/workflows/serialization-schema-check.yaml +++ b/.gitea/workflows/serialization-schema-check.yaml @@ -200,8 +200,7 @@ 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 推送 → 空触发集 → 不告警) + # 对比区间仍为 before→after 全量;责任人触发集=first-parent 非 merge + --author(纯 merge 不告警) java -jar "/tmp/serialization-schema-checker-${SERIALIZATION_SCHEMA_CHECKER_VERSION}.jar" \ --config .gitea/config/serialization-schema-check-config.yaml \ --repo-root . \ diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..d6b5377 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/docs/CI集成说明.md b/docs/CI集成说明.md index 5eab126..1427368 100644 --- a/docs/CI集成说明.md +++ b/docs/CI集成说明.md @@ -45,10 +45,11 @@ mode=block 且含任意结构变更 → exit 1(流水线失败) | 项 | 行为 | |----|------| | 对比区间 | 仍为整次 push 的 `before → after`(Schema 新旧用全量 diff 回填) | -| 触发集 | 仅 **责任人非 merge 提交** 触及的 Java 文件 ∩ 区间 diff | -| 责任人参数 | `--responsible-author`(流水线传 `gitea.actor`);`--modifier` 同为 actor,报告抬头「责任人」 | -| Author 匹配 | 对 `git log` 的 `%an <%ae>` **不区分大小写包含** actor(中文名/邮箱对不上时可能匹配失败) | -| `--no-merges` | 纯 merge 推送 → 触发集为空 → **不告警**(符合前提) | +| 触发集 | 仅 **责任人 first-parent 非 merge 提交** 触及的 Java 文件 ∩ 区间 diff | +| 责任人参数 | `--responsible-author`(流水线传 `gitea.actor`);未传时回退 `--modifier` | +| Author 匹配 | `git log --author` + `--regexp-ignore-case`(子串;特殊字符已转义) | +| `--first-parent` | 大合并不把二路带入的他人历史算进推送人触及集 | +| `--no-merges` | 纯 merge 推送 → 触发集为空 → **不告警** | | 责任人无自有 Java 改动 | 空报告、打日志跳过 | ```bash @@ -170,8 +171,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 子串 | +| 合并推送仍告他人改动 | jar 过旧(未含 first-parent/`--author`) | 重新 deploy 含本次修复的 jar 并确认 Runner 下载到新包 | +| 责任人过滤后漏报自己的改动 | actor 与 commit author/email 对不上 | 看日志「责任人过滤…」;核对 `%an <%ae>` 是否含 actor 子串 | --- diff --git a/pom.xml b/pom.xml index a6e3474..fb764bd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.codechecker serialization-schema-checker - 1.1.0 + 1.0.0 jar serialization-schema-checker diff --git a/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java b/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java index 4ca53c8..a25bed7 100644 --- a/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java +++ b/src/main/java/com/codechecker/cache/analyze/SchemaCheckAnalyzer.java @@ -64,8 +64,8 @@ public class SchemaCheckAnalyzer { * * @param oldSha 基准提交 * @param newSha 当前提交 - * @param responsibleAuthor 责任人过滤(如 Gitea actor);非空时仅将「该作者非 merge 提交触及的文件」 - * 作为检测触发集(他人已在各自 push 告警过的改动不再挂到合并推送人) + * @param responsibleAuthor 责任人过滤(如 Gitea actor);非空时仅将「该作者 first-parent 非 merge + * 提交触及的文件」作为检测触发集(大合并不把二路他人历史算到推送人) * @return 检测报告 * @throws GitException git 命令失败时 */ @@ -92,11 +92,11 @@ public class SchemaCheckAnalyzer { } } changedFiles = scoped; - System.out.println("[serialization-schema-checker] 责任人过滤: " + authorFilter + System.out.println("[serialization-schema-checker] 责任人过滤(first-parent): " + authorFilter + ",区间 diff 文件 " + allDiffFiles.size() + " → 责任人触及 " + changedFiles.size()); if (changedFiles.isEmpty()) { - System.out.println("[serialization-schema-checker] 责任人无非 merge 自有 Java 改动," + System.out.println("[serialization-schema-checker] 责任人无 first-parent 非 merge 自有 Java 改动," + "跳过结构告警(相关变更应已在原作者 push 时通知)。"); return buildReport(oldSha, newSha, Collections.emptyList(), Collections.emptyMap()); } diff --git a/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java b/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java index c744a10..3a86a17 100644 --- a/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java +++ b/src/main/java/com/codechecker/cache/cli/SerializationSchemaCheckerMain.java @@ -53,7 +53,7 @@ public class SerializationSchemaCheckerMain implements Callable { private boolean dryRun; @Option(names = "--responsible-author", - description = "责任人过滤:仅检测该作者非 merge 提交触及的 Java 文件(通常传 Gitea actor)") + description = "责任人过滤:仅检测该作者 first-parent 非 merge 提交触及的 Java 文件(通常传 Gitea actor)") private String responsibleAuthor; @Override diff --git a/src/main/java/com/codechecker/cache/git/GitDiffScanner.java b/src/main/java/com/codechecker/cache/git/GitDiffScanner.java index 7c40654..2623967 100644 --- a/src/main/java/com/codechecker/cache/git/GitDiffScanner.java +++ b/src/main/java/com/codechecker/cache/git/GitDiffScanner.java @@ -10,8 +10,8 @@ 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; +import java.util.regex.Pattern; /** * 基于 git 命令的差异扫描与双版本文件内容读取。 @@ -21,8 +21,8 @@ import java.util.Set; */ public class GitDiffScanner { - /** {@code git log --pretty} 中标识 commit 头的前缀(解析责任人触及文件时用)。 */ - static final String COMMIT_HEADER_PREFIX = "COMMIT "; + /** {@code git log --author} 正则中需转义的元字符。 */ + private static final Pattern AUTHOR_REGEX_META = Pattern.compile("([\\\\.\\[\\]{}()*+?^$|])"); private final Path repoRoot; @@ -47,10 +47,16 @@ public class GitDiffScanner { } /** - * 返回 old..new 区间内、由责任人非 merge提交触及的 Java 文件。 - *

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

+ * 返回 old..new 区间内、责任人在first-parent 非 merge提交中触及的 Java 文件。 * - * @param authorFilter 责任人过滤串(通常为 Gitea actor);与 author name/email 做不区分大小写的包含匹配 + *

用于「合并推送不背锅」:

+ * + * + * @param authorFilter 责任人过滤串(通常为 Gitea actor);对 author name/email 做不区分大小写的子串匹配 * @return 相对路径集合;无匹配时为空 */ public Set javaFilesTouchedByAuthor(String oldSha, String newSha, String authorFilter) @@ -58,39 +64,48 @@ public class GitDiffScanner { if (authorFilter == null || authorFilter.trim().isEmpty()) { return new LinkedHashSet<>(); } + String authorRegex = escapeAuthorRegex(authorFilter.trim()); + // pretty 置空:只输出 --name-only 的路径,交给 collectJavaPathsFromNameOnlyLog List lines = runLines( - "git", "log", "--no-merges", "--name-only", - "--pretty=format:" + COMMIT_HEADER_PREFIX + "%an <%ae>", - oldSha + ".." + newSha, "--", "*.java"); - return parseAuthorTouchedJavaFiles(lines, authorFilter.trim()); + "git", "log", + "--first-parent", + "--no-merges", + "--regexp-ignore-case", + "--author=" + authorRegex, + "--name-only", + "--pretty=format:", + oldSha + ".." + newSha, + "--", "*.java"); + return collectJavaPathsFromNameOnlyLog(lines); } /** - * 解析 {@link #javaFilesTouchedByAuthor} 的 git log 输出。 + * 将责任人过滤串转成 {@code git log --author} 可用的字面量正则(转义元字符)。 */ - static Set parseAuthorTouchedJavaFiles(List lines, String authorFilter) { + static String escapeAuthorRegex(String authorFilter) { + if (authorFilter == null || authorFilter.isEmpty()) { + return authorFilter; + } + return AUTHOR_REGEX_META.matcher(authorFilter).replaceAll("\\\\$1"); + } + + /** + * 解析 {@code git log --name-only --pretty=format:} 输出:收集 .java 路径(author 已由 git 滤掉)。 + */ + static Set collectJavaPathsFromNameOnlyLog(List lines) { Set result = new LinkedHashSet<>(); - if (lines == null || authorFilter == null || authorFilter.isEmpty()) { + if (lines == null) { 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()) { + if (line.isEmpty() || !line.endsWith(".java")) { 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('\\', '/')); - } + result.add(line.replace('\\', '/')); } return result; } diff --git a/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java b/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java index c020c48..605554a 100644 --- a/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java +++ b/src/test/java/com/codechecker/cache/git/GitDiffScannerAuthorFilterTest.java @@ -10,57 +10,45 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * {@link GitDiffScanner#parseAuthorTouchedJavaFiles} 责任人过滤解析单测。 + * 责任人过滤:name-only 路径收集与 {@code --author} 正则转义单测。 */ class GitDiffScannerAuthorFilterTest { @Test - void takesOnlyMatchingAuthorJavaFiles() { - Set files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList( - "COMMIT Alice ", + void collectJavaPathsSkipsNonJavaAndBlanks() { + Set files = GitDiffScanner.collectJavaPathsFromNameOnlyLog(Arrays.asList( "a/src/main/java/A.java", - "a/src/main/java/Shared.java", - "COMMIT Bob ", + "", + "README.md", "b/src/main/java/B.java", - "a/src/main/java/Shared.java" - ), "alice"); + "a/src/main/java/A.java" + )); assertEquals(Set.of( "a/src/main/java/A.java", - "a/src/main/java/Shared.java" + "b/src/main/java/B.java" ), files); } @Test - void matchesActorAgainstEmailCaseInsensitive() { - Set files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList( - "COMMIT 张三 ", - "pkg/Foo.java", - "COMMIT 李四 ", - "pkg/Bar.java" - ), "zhangsan"); - + void collectJavaPathsNormalizesBackslash() { + Set files = GitDiffScanner.collectJavaPathsFromNameOnlyLog(Collections.singletonList( + "pkg\\Foo.java" + )); 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); + void collectJavaPathsNullOrEmptyYieldEmpty() { + assertTrue(GitDiffScanner.collectJavaPathsFromNameOnlyLog(null).isEmpty()); + assertTrue(GitDiffScanner.collectJavaPathsFromNameOnlyLog(Collections.emptyList()).isEmpty()); } @Test - void emptyFilterOrLinesYieldEmpty() { - assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles(Collections.emptyList(), "alice").isEmpty()); - assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles( - Collections.singletonList("COMMIT alice "), "").isEmpty()); + void escapeAuthorRegexEscapesMetacharacters() { + assertEquals("shitou", GitDiffScanner.escapeAuthorRegex("shitou")); + assertEquals("a\\.b", GitDiffScanner.escapeAuthorRegex("a.b")); + assertEquals("x\\+y", GitDiffScanner.escapeAuthorRegex("x+y")); + assertEquals("foo\\[bar\\]", GitDiffScanner.escapeAuthorRegex("foo[bar]")); } }