diff --git a/libraries/tools/kotlin-prepush-hook/build.gradle.kts b/libraries/tools/kotlin-prepush-hook/build.gradle.kts new file mode 100644 index 00000000000..56972e83288 --- /dev/null +++ b/libraries/tools/kotlin-prepush-hook/build.gradle.kts @@ -0,0 +1,40 @@ +import org.gradle.api.Project +import java.io.File + +apply { plugin("java") } +apply { plugin("jps-compatible") } + +jvmTarget = "1.6" + +sourceSets { + "main" { projectDefault() } + "test" {} +} + +project.addPrePushHookIfMissing() + +fun Project.addPrePushHookIfMissing() { + val dotGitDirectory = rootProject.getGitDirectory() + val hooksDirectory = File(dotGitDirectory, "hooks").also { it.mkdirs() } + + val prePushHook = File(projectDir, "pre-push.sh").also { require(it.exists()) } + val prePushTarget = File(hooksDirectory, "pre-push") + prePushHook.copyTo(prePushTarget, overwrite = true) + prePushTarget.setExecutable(true, true) +} + +fun Project.getGitDirectory(): File { + val dotGitFile = File(projectDir, ".git") + + return if (dotGitFile.isFile) { + val workTreeLink = dotGitFile.readLines().single { it.startsWith("gitdir: ") } + val mainRepoPath = workTreeLink + .substringAfter("gitdir: ", "") + .substringBefore("/.git/worktrees/", "") + .also { require(it.isNotEmpty()) } + + File(mainRepoPath, ".git").also { require(it.isDirectory) } + } else { + dotGitFile + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-prepush-hook/pre-push.sh b/libraries/tools/kotlin-prepush-hook/pre-push.sh new file mode 100644 index 00000000000..afc51ec7640 --- /dev/null +++ b/libraries/tools/kotlin-prepush-hook/pre-push.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +targetRepo="$2" + +remoteRefs="" + +javacPath="$JAVA_HOME/bin/javac" +javaPath="$JAVA_HOME/bin/java" + +if [ -f "~/.kotlin-deactivate-prepush-hook" ]; then + echo "Pre-commit hook is deactivated" + exit 0 +fi + +if [ ! -f "./libraries/tools/kotlin-prepush-hook/src/KotlinPrePushHook.java" ]; then + echo "Pre-commit hook .java file was not found in current branch, pre-push hook is disabled" + exit 0 +fi + +if [ ! -f "$javaPath" ]; then + echo "'java' ($javaPath) was not found, pre-push hook is disabled" + exit 0 +fi + +if [ ! -f "$javacPath" ]; then + echo "'javac' ($javacPath) was not found, pre-push hook is disabled" + exit 0 +fi + +while read localRef localSha remoteRef remoteSha +do + # Looks like there're no [[ in Ubuntu by default + if [ "$remoteRef" = "refs/heads/rr/"* ]; then + continue + fi + + if [ "$remoteRef" = "refs/heads/rrr/"* ]; then + continue + fi + + if [ -z $remoteRefs ]; then + remoteRefs="$remoteRef" + else + remoteRefs="$remoteRefs,$remoteRef" + fi +done + +if [ -z "$remoteRefs" ]; then + exit 0 +fi + +mkdir -p ./build/prePushHook +"$javacPath" -d ./build/prePushHook ./libraries/tools/kotlin-prepush-hook/src/KotlinPrePushHook.java +cd ./build/prePushHook + +"$javaPath" KotlinPrePushHook "$remoteRefs" "$targetRepo" +returnCode=$? + +cd ../.. + +exit $returnCode \ No newline at end of file diff --git a/libraries/tools/kotlin-prepush-hook/src/KotlinPrePushHook.java b/libraries/tools/kotlin-prepush-hook/src/KotlinPrePushHook.java new file mode 100644 index 00000000000..e5b8387b520 --- /dev/null +++ b/libraries/tools/kotlin-prepush-hook/src/KotlinPrePushHook.java @@ -0,0 +1,111 @@ +import javax.swing.*; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.*; +import java.util.stream.Collectors; + +public class KotlinPrePushHook { + private static final String BRANCH_DATA_URL = "https://raw.githubusercontent.com/Kotlin/kotlin-branch-status/master/status.txt"; + private static final String MESSAGE_PREFIX = "MESSAGE."; + + private static final List KOTLIN_GIT_REPOS = Arrays.asList( + "git@github.com:JetBrains/kotlin.git", + "https://github.com/JetBrains/kotlin.git"); + + private static final String REF_PREFIX = "refs/heads/"; + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + error("Usage: , got " + Arrays.toString(args)); + } + + List remoteRefs = Arrays + .stream(args[0].split(",")) + .filter(ref -> ref.startsWith(REF_PREFIX)) + .map(ref -> ref.substring(REF_PREFIX.length())).collect(Collectors.toList()); + + String targetRepo = args[1]; + + if (!KOTLIN_GIT_REPOS.contains(targetRepo)) { + return; + } + + check(remoteRefs); + } + + private static void check(List remoteRefs) throws Exception { + Map branchProperties = getProperties(new URL(BRANCH_DATA_URL)); + + Map messages = new HashMap<>(); + Map branches = new HashMap<>(); + + for (Map.Entry entry : branchProperties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + if (key.startsWith(MESSAGE_PREFIX)) { + messages.put(key.substring(MESSAGE_PREFIX.length()), value); + } else { + branches.put(key, value); + } + } + + for (Map.Entry entry : branches.entrySet()) { + String branchName = entry.getKey(); + + if (remoteRefs.contains(branchName)) { + String message = messages.get(entry.getValue()); + if (message == null) { + error("Invalid message key '" + entry.getValue() + "', expected one of these: " + messages.keySet()); + } + + //noinspection ConstantConditions + if (!showWarning(message, branchName)) { + error("Push aborted."); + } + } + } + } + + private static void error(String message) { + //noinspection UseOfSystemOutOrSystemErr + System.err.println(message); + System.exit(1); + } + + private static boolean showWarning(String reason, String targetBranch) { + String baseMessage = "You are about to commit to '" + targetBranch + "',\n" + + "and it is probably not the best idea.\n" + + "Please think twice before pressing 'Yes'."; + + String confirmationMessage = "Do you still want to commit to '" + targetBranch + "'?"; + + String completeMessage = baseMessage + "\n\nReason: " + reason.replace("\\n", "\n") + "\n\n" + confirmationMessage; + + int result = JOptionPane.showOptionDialog( + null, completeMessage, "Friendly warning", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, + new String[] { "Yes", "No" }, "No"); + + return result == JOptionPane.YES_OPTION; + } + + private static Map getProperties(URL url) throws Exception { + URLConnection connection = url.openConnection(); + + try (InputStream is = connection.getInputStream()) { + Properties properties = new Properties(); + properties.load(is); + + Map propertyMap = new HashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + if (entry.getKey() instanceof String && entry.getValue() instanceof String) { + propertyMap.put((String) entry.getKey(), (String) entry.getValue()); + } + } + + return propertyMap; + } + } +} diff --git a/settings.gradle b/settings.gradle index 6982750ecb4..83e7500f6a5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -167,6 +167,7 @@ include ":kotlin-build-common", ":examples:scripting-jvm-maven-deps", ":examples:scripting-jvm-maven-deps-host", ":pill:generate-all-tests", + ":libraries:kotlin-prepush-hook", ":include:kotlin-compiler", // plugin markers: @@ -276,6 +277,7 @@ project(':examples:scripting-jvm-maven-deps').projectDir = "$rootDir/libraries/e project(':examples:scripting-jvm-maven-deps-host').projectDir = "$rootDir/libraries/examples/scripting/jvm-maven-deps/host" as File project(':pill:generate-all-tests').projectDir = "$rootDir/plugins/pill/generate-all-tests" as File project(':kotlin-imports-dumper-compiler-plugin').projectDir = "$rootDir/plugins/imports-dumper" as File +project(':libraries:kotlin-prepush-hook').projectDir = "$rootDir/libraries/tools/kotlin-prepush-hook" as File // plugin markers: project(':kotlin-gradle-plugin:plugin-marker').projectDir = file("$rootDir/libraries/tools/kotlin-gradle-plugin/plugin-marker")