Last active
January 20, 2023 12:46
-
-
Save G00fY2/c004f71d48600197e8b01a7181f77e9a to your computer and use it in GitHub Desktop.
Gradle task to generate a git commit hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package buildplugins | |
import org.gradle.api.DefaultTask | |
import org.gradle.api.GradleException | |
import org.gradle.api.Plugin | |
import org.gradle.api.Project | |
import org.gradle.api.model.ObjectFactory | |
import org.gradle.api.plugins.HelpTasksPlugin.HELP_GROUP | |
import org.gradle.api.provider.Property | |
import org.gradle.api.tasks.Input | |
import org.gradle.api.tasks.TaskAction | |
import org.gradle.internal.jvm.Jvm | |
import java.io.File | |
import java.util.concurrent.TimeUnit | |
import javax.inject.Inject | |
class GitHookPlugin : Plugin<Project> { | |
override fun apply(target: Project) { | |
if (target.rootProject == target) { | |
val ext = target.extensions.create("customGitHook", GitHookPluginExtension::class.java, target.objects) | |
target.tasks.register("installGitCommitMsgHook", InstallGitHookTask::class.java) { | |
description = "Generates a git hook file to run custom gradle tasks" | |
group = HELP_GROUP | |
commitHookTask.set(ext.commitHookTask) | |
} | |
target.tasks.register("commitMessageCheck", CommitMessageCheckTask::class.java) { | |
description = "Checks if commit message matches the defined regex" | |
group = HELP_GROUP | |
commitMsgRegex.set(ext.commitMsgRegex) | |
commitSubjectMaxLength.set(ext.commitSubjectMaxLength) | |
} | |
} | |
} | |
} | |
open class GitHookPluginExtension internal constructor(objectFactory: ObjectFactory) { | |
/** | |
* Task that gets executed as commit-msg hook (in addition to the commitMessageCheck). | |
* Default: detekt | |
*/ | |
val commitHookTask: Property<String> = objectFactory.property(String::class.java).apply { set("detekt") } | |
/** | |
* RegEx pattern for commit message validation. | |
* Default: 'conventional commits' specs | |
*/ | |
val commitMsgRegex: Property<String> = objectFactory.property(String::class.java).apply { | |
set( | |
"^" + | |
"((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\(\\w+\\))?(!)?(: (.*\\s*)*))" + | |
"|(Merge (.*\\s*)*)" + | |
"|(Initial commit\$)" | |
) | |
} | |
/** | |
* Max length of the commit message subject (does not limit multi-line messages). | |
* Default: 72 char's | |
*/ | |
val commitSubjectMaxLength: Property<Int> = objectFactory.property(Int::class.java).apply { set(72) } | |
} | |
open class CommitMessageCheckTask @Inject constructor(objectFactory: ObjectFactory) : DefaultTask() { | |
@get:Input | |
internal val commitMsgRegex: Property<String> = objectFactory.property(String::class.java) | |
@get:Input | |
internal val commitSubjectMaxLength: Property<Int> = objectFactory.property(Int::class.java) | |
@TaskAction | |
fun checkMessage() { | |
val commitMsgParam = project.gradle.startParameter.projectProperties[COMMIT_MSG_FILE] | |
val commitMsgFile = File("${project.projectDir}/$commitMsgParam") | |
if (!commitMsgFile.isFile) throw GradleException("Commit message file not found") | |
val commitMsg = commitMsgFile.readText().removeFinalNewline() | |
if (Regex(commitMsgRegex.get()).matches(commitMsg) && | |
commitMsg.lines().first().length <= commitSubjectMaxLength.get() | |
) { | |
logger.info("Valid commit message") | |
} else { | |
throw GradleException("Commit message `$commitMsg` does not match conventional commits") | |
} | |
} | |
private fun String.removeFinalNewline(): String { | |
return if (isNotEmpty() && listOf("\r\n", "\n", "\r").contains(last().toString())) dropLast(1) | |
else this | |
} | |
} | |
open class InstallGitHookTask @Inject constructor(objectFactory: ObjectFactory) : DefaultTask() { | |
@get:Input | |
internal val commitHookTask: Property<String> = objectFactory.property(String::class.java) | |
@TaskAction | |
fun installHook() { | |
val gitHookPath = "git rev-parse --path-format=absolute --git-path hooks".executeCommand() | |
val commitMsgHookFile = File("$gitHookPath/commit-msg") | |
logger.info("Hook file: $commitMsgHookFile") | |
commitMsgHookFile.parentFile.mkdirs() | |
if (!commitMsgHookFile.isFile) { | |
commitMsgHookFile.createNewFile() | |
commitMsgHookFile.setExecutable(true) | |
} | |
var currentFileContent = commitMsgHookFile.readText() | |
if (currentFileContent.isBlank()) { | |
commitMsgHookFile.writeText("$shShebang$STARTHOOKSECTION${generateGitHook(commitHookTask.get())}$ENDHOOKSECTION") | |
logger.info("Hook added to empty file") | |
} else if (!currentFileContent.contains(STARTHOOKSECTION)) { | |
commitMsgHookFile.appendText("$STARTHOOKSECTION${generateGitHook(commitHookTask.get())}$ENDHOOKSECTION") | |
logger.info("Hook appended to file") | |
} else { | |
currentFileContent = currentFileContent.replaceRange( | |
currentFileContent.indexOf(STARTHOOKSECTION), | |
currentFileContent.indexOf(ENDHOOKSECTION), | |
"$STARTHOOKSECTION${generateGitHook(commitHookTask.get())}" | |
) | |
commitMsgHookFile.writeText(currentFileContent) | |
logger.info("Hook replaced in file") | |
} | |
} | |
private fun generateGitHook(taskCommand: String): String { | |
val taskArguments = mutableListOf( | |
"./gradlew", | |
"commitMessageCheck -P$COMMIT_MSG_FILE=$1", | |
taskCommand, | |
"-Dorg.gradle.java.home=${Jvm.current().javaHome}" | |
).filter { it.isNotBlank() } | |
logger.info("Hook task: ${taskArguments.joinToString(separator = " ")}") | |
return """ | |
echo "Running custom gradle plugin hook." | |
${taskArguments.joinToString(separator = " ")} | |
echo "Completed custom gradle plugin hook." | |
""".trimIndent() | |
} | |
private fun String.executeCommand(): String { | |
val builder = ProcessBuilder(split("\\s".toRegex())) | |
return try { | |
builder.start().let { process -> | |
process.waitFor(10, TimeUnit.SECONDS) | |
if (process.exitValue() == 0) process.inputStream.bufferedReader().use { it.readText().trim() } | |
else throw GradleException("Error executing command: '${builder.command().joinToString(" ")}'") | |
} | |
} catch (e: Exception) { | |
throw GradleException("${e.message ?: ""} Error executing command: '${builder.command().joinToString(" ")}'") | |
} | |
} | |
private val shShebang = | |
""" | |
#!/bin/sh | |
set -e | |
""".trimIndent() | |
} | |
private const val COMMIT_MSG_FILE = "commitMsgFile" | |
private const val STARTHOOKSECTION = "######## CUSTOM GRADLE HOOK START ########\n" | |
private const val ENDHOOKSECTION = "######## CUSTOM GRADLE HOOK END ########\n" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment