feat: V1.1 - 责任人区分,多次合并时误触修复
All checks were successful
序列化结构检查 / serialization-schema-check (push) Successful in 2s
All checks were successful
序列化结构检查 / serialization-schema-check (push) Successful in 2s
This commit is contained in:
@@ -200,8 +200,7 @@ jobs:
|
|||||||
|
|
||||||
COMMIT_TIME=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S' "${NEW_SHA}")
|
COMMIT_TIME=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S' "${NEW_SHA}")
|
||||||
|
|
||||||
# 对比区间仍为 before→after 全量;--responsible-author 只把推送人非 merge 触及的文件作触发集
|
# 对比区间仍为 before→after 全量;责任人触发集=first-parent 非 merge + --author(纯 merge 不告警)
|
||||||
# (他人改动应已在其各自 push 时告警;纯 merge 推送 → 空触发集 → 不告警)
|
|
||||||
java -jar "/tmp/serialization-schema-checker-${SERIALIZATION_SCHEMA_CHECKER_VERSION}.jar" \
|
java -jar "/tmp/serialization-schema-checker-${SERIALIZATION_SCHEMA_CHECKER_VERSION}.jar" \
|
||||||
--config .gitea/config/serialization-schema-check-config.yaml \
|
--config .gitea/config/serialization-schema-check-config.yaml \
|
||||||
--repo-root . \
|
--repo-root . \
|
||||||
|
|||||||
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/tmp-shallow2/repo" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -45,10 +45,11 @@ mode=block 且含任意结构变更 → exit 1(流水线失败)
|
|||||||
| 项 | 行为 |
|
| 项 | 行为 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| 对比区间 | 仍为整次 push 的 `before → after`(Schema 新旧用全量 diff 回填) |
|
| 对比区间 | 仍为整次 push 的 `before → after`(Schema 新旧用全量 diff 回填) |
|
||||||
| 触发集 | 仅 **责任人非 merge 提交** 触及的 Java 文件 ∩ 区间 diff |
|
| 触发集 | 仅 **责任人 first-parent 非 merge 提交** 触及的 Java 文件 ∩ 区间 diff |
|
||||||
| 责任人参数 | `--responsible-author`(流水线传 `gitea.actor`);`--modifier` 同为 actor,报告抬头「责任人」 |
|
| 责任人参数 | `--responsible-author`(流水线传 `gitea.actor`);未传时回退 `--modifier` |
|
||||||
| Author 匹配 | 对 `git log` 的 `%an <%ae>` **不区分大小写包含** actor(中文名/邮箱对不上时可能匹配失败) |
|
| Author 匹配 | `git log --author` + `--regexp-ignore-case`(子串;特殊字符已转义) |
|
||||||
| `--no-merges` | 纯 merge 推送 → 触发集为空 → **不告警**(符合前提) |
|
| `--first-parent` | 大合并不把二路带入的他人历史算进推送人触及集 |
|
||||||
|
| `--no-merges` | 纯 merge 推送 → 触发集为空 → **不告警** |
|
||||||
| 责任人无自有 Java 改动 | 空报告、打日志跳过 |
|
| 责任人无自有 Java 改动 | 空报告、打日志跳过 |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -170,8 +171,8 @@ com/codechecker/serialization-schema-checker/1.0.0/
|
|||||||
| commit 数显示为 1(实际多个) | 浅克隆下 `rev-list` 看不到中间提交 | 已修复:优先事件 `commits` 长度;并 deepen 到 before 为祖先 |
|
| commit 数显示为 1(实际多个) | 浅克隆下 `rev-list` 看不到中间提交 | 已修复:优先事件 `commits` 长度;并 deepen 到 before 为祖先 |
|
||||||
| 漏报(模式/模块) | W0x/MQ 未开 / include_modules 过窄 | 确认 W01~W05 与 mq_patterns(含 MQ03~05、MQ-K03/K04);检查 `mq_read_hints_enabled` 与模块过滤 |
|
| 漏报(模式/模块) | W0x/MQ 未开 / include_modules 过窄 | 确认 W01~W05 与 mq_patterns(含 MQ03~05、MQ-K03/K04);检查 `mq_read_hints_enabled` 与模块过滤 |
|
||||||
| 类型展开不完整 | 类型在依赖 jar 中 | 补充 `manual_mappings.value_type` |
|
| 类型展开不完整 | 类型在依赖 jar 中 | 补充 `manual_mappings.value_type` |
|
||||||
| 合并推送仍告他人改动 | 未传 `--responsible-author` / jar 过旧 | 同步流水线模板;确认 jar ≥ 含责任人过滤的版本 |
|
| 合并推送仍告他人改动 | jar 过旧(未含 first-parent/`--author`) | 重新 deploy 含本次修复的 jar 并确认 Runner 下载到新包 |
|
||||||
| 责任人过滤后漏报自己的改动 | actor 与 `%an <%ae>` 对不上 | 看日志「责任人过滤…」;核对 commit author 是否含 actor 子串 |
|
| 责任人过滤后漏报自己的改动 | actor 与 commit author/email 对不上 | 看日志「责任人过滤…」;核对 `%an <%ae>` 是否含 actor 子串 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.codechecker</groupId>
|
<groupId>com.codechecker</groupId>
|
||||||
<artifactId>serialization-schema-checker</artifactId>
|
<artifactId>serialization-schema-checker</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>serialization-schema-checker</name>
|
<name>serialization-schema-checker</name>
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ public class SchemaCheckAnalyzer {
|
|||||||
*
|
*
|
||||||
* @param oldSha 基准提交
|
* @param oldSha 基准提交
|
||||||
* @param newSha 当前提交
|
* @param newSha 当前提交
|
||||||
* @param responsibleAuthor 责任人过滤(如 Gitea actor);非空时仅将「该作者非 merge 提交触及的文件」
|
* @param responsibleAuthor 责任人过滤(如 Gitea actor);非空时仅将「该作者 first-parent 非 merge
|
||||||
* 作为检测触发集(他人已在各自 push 告警过的改动不再挂到合并推送人)
|
* 提交触及的文件」作为检测触发集(大合并不把二路他人历史算到推送人)
|
||||||
* @return 检测报告
|
* @return 检测报告
|
||||||
* @throws GitException git 命令失败时
|
* @throws GitException git 命令失败时
|
||||||
*/
|
*/
|
||||||
@@ -92,11 +92,11 @@ public class SchemaCheckAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
changedFiles = scoped;
|
changedFiles = scoped;
|
||||||
System.out.println("[serialization-schema-checker] 责任人过滤: " + authorFilter
|
System.out.println("[serialization-schema-checker] 责任人过滤(first-parent): " + authorFilter
|
||||||
+ ",区间 diff 文件 " + allDiffFiles.size()
|
+ ",区间 diff 文件 " + allDiffFiles.size()
|
||||||
+ " → 责任人触及 " + changedFiles.size());
|
+ " → 责任人触及 " + changedFiles.size());
|
||||||
if (changedFiles.isEmpty()) {
|
if (changedFiles.isEmpty()) {
|
||||||
System.out.println("[serialization-schema-checker] 责任人无非 merge 自有 Java 改动,"
|
System.out.println("[serialization-schema-checker] 责任人无 first-parent 非 merge 自有 Java 改动,"
|
||||||
+ "跳过结构告警(相关变更应已在原作者 push 时通知)。");
|
+ "跳过结构告警(相关变更应已在原作者 push 时通知)。");
|
||||||
return buildReport(oldSha, newSha, Collections.emptyList(), Collections.emptyMap());
|
return buildReport(oldSha, newSha, Collections.emptyList(), Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class SerializationSchemaCheckerMain implements Callable<Integer> {
|
|||||||
private boolean dryRun;
|
private boolean dryRun;
|
||||||
|
|
||||||
@Option(names = "--responsible-author",
|
@Option(names = "--responsible-author",
|
||||||
description = "责任人过滤:仅检测该作者非 merge 提交触及的 Java 文件(通常传 Gitea actor)")
|
description = "责任人过滤:仅检测该作者 first-parent 非 merge 提交触及的 Java 文件(通常传 Gitea actor)")
|
||||||
private String responsibleAuthor;
|
private String responsibleAuthor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于 git 命令的差异扫描与双版本文件内容读取。
|
* 基于 git 命令的差异扫描与双版本文件内容读取。
|
||||||
@@ -21,8 +21,8 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public class GitDiffScanner {
|
public class GitDiffScanner {
|
||||||
|
|
||||||
/** {@code git log --pretty} 中标识 commit 头的前缀(解析责任人触及文件时用)。 */
|
/** {@code git log --author} 正则中需转义的元字符。 */
|
||||||
static final String COMMIT_HEADER_PREFIX = "COMMIT ";
|
private static final Pattern AUTHOR_REGEX_META = Pattern.compile("([\\\\.\\[\\]{}()*+?^$|])");
|
||||||
|
|
||||||
private final Path repoRoot;
|
private final Path repoRoot;
|
||||||
|
|
||||||
@@ -47,10 +47,16 @@ public class GitDiffScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回 old..new 区间内、由责任人<strong>非 merge</strong>提交触及的 Java 文件。
|
* 返回 old..new 区间内、责任人在<strong>first-parent 非 merge</strong>提交中触及的 Java 文件。
|
||||||
* <p>用于「作者 push 已告警、合并推送人不再背锅」:只把责任人自己的改动纳入检测触发集。</p>
|
|
||||||
*
|
*
|
||||||
* @param authorFilter 责任人过滤串(通常为 Gitea actor);与 author name/email 做不区分大小写的包含匹配
|
* <p>用于「合并推送不背锅」:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --first-parent}:不把二路合并带入的他人历史算进责任人触及集</li>
|
||||||
|
* <li>{@code --no-merges}:纯 merge 推送 → 空集 → 上层跳过告警</li>
|
||||||
|
* <li>{@code --author}:由 git 按 author 过滤,避免自解析 pretty 在浅克隆下粘滞误归属</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param authorFilter 责任人过滤串(通常为 Gitea actor);对 author name/email 做不区分大小写的子串匹配
|
||||||
* @return 相对路径集合;无匹配时为空
|
* @return 相对路径集合;无匹配时为空
|
||||||
*/
|
*/
|
||||||
public Set<String> javaFilesTouchedByAuthor(String oldSha, String newSha, String authorFilter)
|
public Set<String> javaFilesTouchedByAuthor(String oldSha, String newSha, String authorFilter)
|
||||||
@@ -58,39 +64,48 @@ public class GitDiffScanner {
|
|||||||
if (authorFilter == null || authorFilter.trim().isEmpty()) {
|
if (authorFilter == null || authorFilter.trim().isEmpty()) {
|
||||||
return new LinkedHashSet<>();
|
return new LinkedHashSet<>();
|
||||||
}
|
}
|
||||||
|
String authorRegex = escapeAuthorRegex(authorFilter.trim());
|
||||||
|
// pretty 置空:只输出 --name-only 的路径,交给 collectJavaPathsFromNameOnlyLog
|
||||||
List<String> lines = runLines(
|
List<String> lines = runLines(
|
||||||
"git", "log", "--no-merges", "--name-only",
|
"git", "log",
|
||||||
"--pretty=format:" + COMMIT_HEADER_PREFIX + "%an <%ae>",
|
"--first-parent",
|
||||||
oldSha + ".." + newSha, "--", "*.java");
|
"--no-merges",
|
||||||
return parseAuthorTouchedJavaFiles(lines, authorFilter.trim());
|
"--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<String> parseAuthorTouchedJavaFiles(List<String> 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<String> collectJavaPathsFromNameOnlyLog(List<String> lines) {
|
||||||
Set<String> result = new LinkedHashSet<>();
|
Set<String> result = new LinkedHashSet<>();
|
||||||
if (lines == null || authorFilter == null || authorFilter.isEmpty()) {
|
if (lines == null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
String needle = authorFilter.toLowerCase(Locale.ROOT);
|
|
||||||
boolean take = false;
|
|
||||||
for (String raw : lines) {
|
for (String raw : lines) {
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String line = raw.trim();
|
String line = raw.trim();
|
||||||
if (line.isEmpty()) {
|
if (line.isEmpty() || !line.endsWith(".java")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.startsWith(COMMIT_HEADER_PREFIX)) {
|
result.add(line.replace('\\', '/'));
|
||||||
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,57 +10,45 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link GitDiffScanner#parseAuthorTouchedJavaFiles} 责任人过滤解析单测。
|
* 责任人过滤:name-only 路径收集与 {@code --author} 正则转义单测。
|
||||||
*/
|
*/
|
||||||
class GitDiffScannerAuthorFilterTest {
|
class GitDiffScannerAuthorFilterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void takesOnlyMatchingAuthorJavaFiles() {
|
void collectJavaPathsSkipsNonJavaAndBlanks() {
|
||||||
Set<String> files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList(
|
Set<String> files = GitDiffScanner.collectJavaPathsFromNameOnlyLog(Arrays.asList(
|
||||||
"COMMIT Alice <alice@example.com>",
|
|
||||||
"a/src/main/java/A.java",
|
"a/src/main/java/A.java",
|
||||||
"a/src/main/java/Shared.java",
|
"",
|
||||||
"COMMIT Bob <bob@example.com>",
|
"README.md",
|
||||||
"b/src/main/java/B.java",
|
"b/src/main/java/B.java",
|
||||||
"a/src/main/java/Shared.java"
|
"a/src/main/java/A.java"
|
||||||
), "alice");
|
));
|
||||||
|
|
||||||
assertEquals(Set.of(
|
assertEquals(Set.of(
|
||||||
"a/src/main/java/A.java",
|
"a/src/main/java/A.java",
|
||||||
"a/src/main/java/Shared.java"
|
"b/src/main/java/B.java"
|
||||||
), files);
|
), files);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void matchesActorAgainstEmailCaseInsensitive() {
|
void collectJavaPathsNormalizesBackslash() {
|
||||||
Set<String> files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList(
|
Set<String> files = GitDiffScanner.collectJavaPathsFromNameOnlyLog(Collections.singletonList(
|
||||||
"COMMIT 张三 <ZhangSan@corp.com>",
|
"pkg\\Foo.java"
|
||||||
"pkg/Foo.java",
|
));
|
||||||
"COMMIT 李四 <lisi@corp.com>",
|
|
||||||
"pkg/Bar.java"
|
|
||||||
), "zhangsan");
|
|
||||||
|
|
||||||
assertEquals(Set.of("pkg/Foo.java"), files);
|
assertEquals(Set.of("pkg/Foo.java"), files);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void skipsNonJavaAndEmptyLines() {
|
void collectJavaPathsNullOrEmptyYieldEmpty() {
|
||||||
Set<String> files = GitDiffScanner.parseAuthorTouchedJavaFiles(Arrays.asList(
|
assertTrue(GitDiffScanner.collectJavaPathsFromNameOnlyLog(null).isEmpty());
|
||||||
"COMMIT alice <a@x.com>",
|
assertTrue(GitDiffScanner.collectJavaPathsFromNameOnlyLog(Collections.emptyList()).isEmpty());
|
||||||
"",
|
|
||||||
"README.md",
|
|
||||||
"src/Main.java",
|
|
||||||
"COMMIT other <o@x.com>",
|
|
||||||
"src/Other.java"
|
|
||||||
), "alice");
|
|
||||||
|
|
||||||
assertEquals(Set.of("src/Main.java"), files);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void emptyFilterOrLinesYieldEmpty() {
|
void escapeAuthorRegexEscapesMetacharacters() {
|
||||||
assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles(Collections.emptyList(), "alice").isEmpty());
|
assertEquals("shitou", GitDiffScanner.escapeAuthorRegex("shitou"));
|
||||||
assertTrue(GitDiffScanner.parseAuthorTouchedJavaFiles(
|
assertEquals("a\\.b", GitDiffScanner.escapeAuthorRegex("a.b"));
|
||||||
Collections.singletonList("COMMIT alice <a@x.com>"), "").isEmpty());
|
assertEquals("x\\+y", GitDiffScanner.escapeAuthorRegex("x+y"));
|
||||||
|
assertEquals("foo\\[bar\\]", GitDiffScanner.escapeAuthorRegex("foo[bar]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user