blob: fbf78f2fe9e4698271d736d510582a0b51c943b1 [file] [edit]
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.nio.file.Files
import java.nio.file.Paths
/**
* See https://github.com/gradle/gradle-private/issues/3919
*
* When merging `releaseX` branch into `master`, we should only use the release note from the `master` branch,
* but sometimes changes on release notes.md was brought to master and merged unnoticed,
* e.g https://github.com/gradle/gradle/pull/25825
*
* This script is to check if there is any merge commit that brings changes from release branch to master.
* Usage: groovy CheckBadMerge.groovy <commit1> <commit2> ...
* If any "bad" merge commit is found, it will print the details and exit with non-zero code.
*/
class CheckBadMerge {
private static final THREAD_POOL = Executors.newCachedThreadPool()
private static final List<String> MONITORED_PATHS = [
"subprojects/docs/src/docs/release/notes.md",
"platforms/documentation/docs/src/docs/release/notes.md",
"platforms/documentation/docs/src/docs/release/release-notes-assets/",
"subprojects/launcher/src/main/resources/release-features.txt",
"platforms/core-runtime/launcher/src/main/resources/release-features.txt"
]
static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: groovy CheckBadMerge.groovy <commits_file>")
System.exit(1)
}
List<String> commits = Files.readAllLines(Paths.get(args[0]))
try {
commits.each { checkCommit(it) }
} finally {
THREAD_POOL.shutdown()
}
}
static void checkCommit(String commit) {
List<String> parentCommits = parentCommitsOf(commit)
if (parentCommits.size() != 2) {
println("$commit is not a merge commit we're looking for. Parents: $parentCommits")
return
}
List<String> commitBranches = branchesOf(commit)
if (commitBranches.contains("origin/release")) {
println("$commit is a merge commit already on release, ignoring.")
println(" Branches: $commitBranches")
return
}
// The correct state we are looking for is:
// 1. It's a merge commit.
// 2. One of its parent commits is from master only.
// 3. Another parent commit is not from master but from release branch.
// Otherwise, skip this commit.
List<String> p1Branches = branchesOf(parentCommits[0])
List<String> p2Branches = branchesOf(parentCommits[1])
println("$commit parents: $parentCommits")
println(" p1Branches: $p1Branches")
println(" p2Branches: $p2Branches")
if (p1Branches.contains("origin/master") && !p2Branches.contains("origin/master") && p2Branches.any { it.startsWith("origin/release") }) {
List<String> badFiles = filesFromMerge(commit).findAll {gitFile -> MONITORED_PATHS.any { forbiddenPath -> gitFile.startsWith(forbiddenPath)} }
if (!badFiles.empty) {
System.err.println("Found bad files in merge commit $commit, run the listed commands:")
badFiles.each {
System.err.println("git restore --source=master -SW -- '$it'")
}
System.err.println("And then amend the merge commit to remove all offending changes.")
System.exit(1)
} else {
println(" -> No bad files found")
}
} else {
println(" -> is not a merge commit we're looking for.")
}
}
static List<String> filesFromMerge(String commit) {
getStdout("git diff --name-only $commit^1..$commit").readLines()
}
static List<String> branchesOf(String commit) {
return getStdout("git branch -r --contains $commit")
.readLines()
.collect { it.replace("*", "") } // remove the * from the current branch, e.g. * master -> master
.collect { it.trim() }
.grep { !it.isEmpty() }
}
static List<String> parentCommitsOf(String commit) {
return getStdout("git show --format=%P --no-patch --no-show-signature $commit")
.split(" ").collect { it.trim() }.grep { !it.isEmpty() }
}
@groovy.transform.ToString
static class ExecResult {
String stdout
String stderr
int returnCode
}
static ExecResult exec(String command) {
Process process = command.execute()
def stdoutFuture = readStreamAsync(process.inputStream)
def stderrFuture = readStreamAsync(process.errorStream)
int returnCode = process.waitFor()
String stdout = stdoutFuture.get()
String stderr = stderrFuture.get()
return new ExecResult(stderr: stderr, stdout: stdout, returnCode: returnCode)
}
static String getStdout(String command) {
ExecResult execResult = exec(command)
assert execResult.returnCode == 0: "$command failed with return code: $execResult"
return execResult.stdout
}
static Future<String> readStreamAsync(InputStream inputStream) {
return THREAD_POOL.submit({ inputStream.text } as Callable) as Future<String>
}
}