Skip to content

Commit 3bd8e2d

Browse files
authored
Merge pull request #6630 from mbien/ci-email-checker
Try to validate commit headers.
2 parents 58eaa73 + e037b87 commit 3bd8e2d

2 files changed

Lines changed: 181 additions & 12 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.IOException;
21+
import java.net.URI;
22+
import java.net.http.HttpClient;
23+
import java.net.http.HttpClient.Redirect;
24+
import java.net.http.HttpRequest;
25+
import java.net.http.HttpResponse.BodyHandlers;
26+
import java.time.Duration;
27+
import java.util.List;
28+
29+
import static java.util.stream.Gatherers.fold;
30+
import static java.util.stream.Gatherers.scan;
31+
import static java.util.stream.Gatherers.windowSliding;
32+
33+
record Commit(int index, String from, String date, String subject, String blank) {}
34+
record Result(int total, boolean green) {}
35+
36+
// checks commit headers for valid author, email and commit msg formatting
37+
// its main purpose is to prevent common merge mistakes
38+
39+
// Java 23+, may require preview flag
40+
// java CommitHeaderChecker.java https://github.com/apache/netbeans/pull/${{ github.event.pull_request.number }}
41+
42+
// green tests:
43+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/66
44+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/7641
45+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/4138
46+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/4692
47+
48+
// red tests:
49+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/7776
50+
// java --enable-preview CommitHeaderChecker.java https://github.com/apache/netbeans/pull/5567
51+
52+
void main(String[] args) throws IOException, InterruptedException {
53+
54+
if (args.length != 1 || !args[0].startsWith("https://github.com/")) {
55+
throw new IllegalArgumentException("PR URL expected");
56+
}
57+
58+
HttpRequest request = HttpRequest.newBuilder()
59+
.uri(URI.create(args[0]+".patch"))
60+
.timeout(Duration.ofSeconds(10))
61+
.build();
62+
63+
println("checking PR patch file...");
64+
Result result;
65+
try (HttpClient client = HttpClient.newBuilder()
66+
.followRedirects(Redirect.NORMAL).build()) {
67+
68+
result = client.send(request, BodyHandlers.ofLines()).body()
69+
// 5 line window, From/Date/Subject and extra line for blank line / overflow check
70+
// "From" can be two lines if the name is very long
71+
.gather(windowSliding(5))
72+
.filter(w -> isCommitHeader(w))
73+
.gather(scan(
74+
() -> new Commit(-1, "", "", "", ""),
75+
(c, w) -> createCommit(c.index+1, w)))
76+
.peek(System.out::println)
77+
.gather(fold(
78+
() -> new Result(0, true),
79+
(r, c) -> new Result(r.total+1, r.green & checkCommit(c))))
80+
.findFirst()
81+
.orElseThrow();
82+
}
83+
84+
println(result.total + " commit(s) checked");
85+
System.exit(result.green ? 0 : 1);
86+
}
87+
88+
// From: Duke <duke42@dukemail.com>
89+
// Date: Thu, 1 Oct 2024 22:10:50 -0700
90+
// Subject: [PATCH] Mail Validator
91+
private static boolean isCommitHeader(List<String> lines) {
92+
int i = 0;
93+
return lines.size() == 5
94+
&& lines.get(i++).startsWith("From: ") // "From" can be two lines in some cases
95+
&&(lines.get(i++).startsWith("Date: ") || lines.get(i++).startsWith("Date: "))
96+
&& lines.get(i++).startsWith("Subject: ");
97+
}
98+
99+
private static Commit createCommit(int index, List<String> lines) {
100+
int i = 0;
101+
return lines.get(1).startsWith("Date: ") // "From" can be two lines in some cases
102+
? new Commit(index, lines.get(i++), lines.get(i++), lines.get(i++), lines.get(i++))
103+
: new Commit(index, lines.get(i++) + lines.get(i++), lines.get(i++), lines.get(i++), lines.get(i++));
104+
}
105+
106+
boolean checkCommit(Commit c) {
107+
return checkNameAndEmail(c.index, c.from)
108+
& checkSubject(c.index, c.subject)
109+
& checkBlankLineAfterSubject(c.index, c.blank);
110+
}
111+
112+
boolean checkNameAndEmail(int i, String from) {
113+
// From: Duke <duke42@dukemail.com>
114+
int start = from.indexOf('<');
115+
int end = from.indexOf('>');
116+
117+
String mail = end > start ? from.substring(start+1, end) : "";
118+
String author = start > 6 ? from.substring(6, start).strip() : "";
119+
120+
// bots may pass
121+
if (author.contains("[bot]")) {
122+
return true;
123+
}
124+
125+
boolean green = true;
126+
if (mail.isBlank() || !mail.contains("@") || mail.contains("noreply") || mail.contains("localhost")) {
127+
println("::error::invalid email in commit " + i + " '" + from + "'");
128+
green = false;
129+
}
130+
131+
// mime encoding indicates it is probably a proper name, since gh account names aren't encoded
132+
boolean encoded = author.startsWith("=?") && author.endsWith("?=");
133+
134+
// single word author -> probably the nickname/account name/root etc
135+
if (author.isBlank() || (!encoded && !author.contains(" ") && !author.contains("-"))) {
136+
println("::error::invalid author in commit " + i + " '" + author + "' (full name?)");
137+
green = false;
138+
}
139+
return green;
140+
}
141+
142+
// https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-commit.html#_discussion
143+
boolean checkSubject(int i, String subject) {
144+
// Subject: [PATCH] msg
145+
subject = subject.substring(subject.indexOf(']')+1).strip();
146+
// single word subjects are likely not intended or should be squashed before merge
147+
if (!subject.contains(" ")) {
148+
println("::error::invalid subject in commit " + i + " '" + subject + "'");
149+
return false;
150+
}
151+
return true;
152+
}
153+
154+
// there should be a blank line after the subject line, some subjects can overflow though.
155+
boolean checkBlankLineAfterSubject(int i, String blank) {
156+
// disabled since this would produce too many warnings due to overflowing subject lines
157+
// if (!blank.isBlank()) {
158+
// println("::warning::blank line after subject recommended in commit " + i + " (is subject over 50 char limit?)");
159+
//// return false;
160+
// }
161+
return true;
162+
}

.github/workflows/main.yml

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,25 +296,31 @@ jobs:
296296
echo "::error::PRs must be labeled, see: https://cwiki.apache.org/confluence/display/NETBEANS/PRs+and+You+-+A+reviewer+Guide"
297297
exit 1
298298
299-
- name: Set up JDK ${{ matrix.java }}
300-
if: ${{ !cancelled() }}
301-
uses: actions/setup-java@v4
302-
with:
303-
java-version: ${{ matrix.java }}
304-
distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }}
305-
306299
- name: Checkout ${{ github.ref }} ( ${{ github.sha }} )
307300
if: ${{ !cancelled() }}
308301
uses: actions/checkout@v4
309302
with:
310303
persist-credentials: false
311304
submodules: false
312305
show-progress: false
313-
fetch-depth: 10
314306

315-
- name: Print last 10 Commits
307+
- name: Set up JDK 23 for scripts
316308
if: ${{ github.event_name == 'pull_request' && !cancelled() }}
317-
run: git log --oneline -n10 --pretty=format:'%h %an [%ae] %s'
309+
uses: actions/setup-java@v4
310+
with:
311+
java-version: 23
312+
distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }}
313+
314+
- name: Check Commit Headers
315+
if: ${{ github.event_name == 'pull_request' && !cancelled() }}
316+
run: java --enable-preview .github/scripts/CommitHeaderChecker.java ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}
317+
318+
- name: Set up JDK ${{ matrix.java }}
319+
if: ${{ !cancelled() }}
320+
uses: actions/setup-java@v4
321+
with:
322+
java-version: ${{ matrix.java }}
323+
distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }}
318324

319325
- name: Check line endings and verify RAT report
320326
if: ${{ !cancelled() }}
@@ -2616,13 +2622,14 @@ jobs:
26162622
env "netbeans.extra.options=-J-Dnetbeans.logger.console=true" ant $OPTS test-vscode-ext
26172623
26182624

2619-
# last job depends on everything so that it is forced to run last even if a long job fails early
2625+
# cleanup job depends on everything so that it is forced to run last even if a long job fails early.
2626+
# 'paperwork' is left out intentionally, since it doesn't run unit tests (hopefully doesn't need restarts)
2627+
# and shouldn't prevent cleanup on validation failure - which might be common during dev time
26202628
cleanup:
26212629
name: Cleanup Workflow Artifacts
26222630
needs:
26232631
- base-build
26242632
- commit-validation
2625-
- paperwork
26262633
- build-system-test
26272634
- build-from-src-zip
26282635
- ide-modules-test

0 commit comments

Comments
 (0)