Skip to content

Instantly share code, notes, and snippets.

@G00fY2
Last active January 20, 2023 12:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save G00fY2/c004f71d48600197e8b01a7181f77e9a to your computer and use it in GitHub Desktop.
Save G00fY2/c004f71d48600197e8b01a7181f77e9a to your computer and use it in GitHub Desktop.
Gradle task to generate a git commit hook
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