Skip to content

Commit

Permalink
feat: install git hooks by symlinking to .githooks
Browse files Browse the repository at this point in the history
  • Loading branch information
gtramontina committed May 27, 2019
1 parent 8140c26 commit 38b3028
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 76 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,5 +1,6 @@
.idea/
.gradle/
.kotlintest/
/build/
node_modules/
!gradle-wrapper.jar
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

21 changes: 17 additions & 4 deletions .travis.yml
@@ -1,9 +1,22 @@
os:
- linux
- osx
language: java
jdk:
- openjdk11
script:
- ./gradlew clean build
after_success:
- nvm install
- yarn install
- yarn semantic-release

jobs:

include:
- stage: release
deploy:
provider: script
skip_cleanup: true
script:
- nvm install lts/*
- yarn install
- yarn semantic-release
on:
all_branches: true
72 changes: 72 additions & 0 deletions build.gradle
@@ -0,0 +1,72 @@
import com.gradle.publish.LoginTask
import com.gradle.publish.PublishTask

import static java.lang.System.*

plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.31"
id "com.adarshr.test-logger" version "1.6.0"
id "org.jlleitschuh.gradle.ktlint" version "7.2.1"
id "com.gradle.plugin-publish" version "0.10.1"
id "java-gradle-plugin"
}

group = "com.gtramontina.ghooks.gradle"
compileKotlin { kotlinOptions.jvmTarget = "1.8" }
compileTestKotlin { kotlinOptions.jvmTarget = "1.8" }
repositories { jcenter() }
dependencies {
implementation(group: "org.jetbrains.kotlin", name: "kotlin-stdlib-jdk8", version: "1.3.31")
testImplementation(group: "io.kotlintest", name: "kotlintest-runner-junit5", version: "3.3.2")
testImplementation(group: "org.junit.jupiter", name: "junit-jupiter-api", version: "5.4.1")
testImplementation(group: "org.awaitility", name: "awaitility", version: "3.1.6")
testRuntime(group: "org.junit.platform", name: "junit-platform-launcher", version: "1.4.1")
testRuntime(group: "org.junit.jupiter", name: "junit-jupiter-engine", version: "5.4.1")
testRuntime(group: "org.junit.vintage", name: "junit-vintage-engine", version: "5.4.1")
}

test { useJUnitPlatform() }

// Plugin Definition ---------------------------------------------------------------------------------------------------

gradlePlugin {
plugins {
create(rootProject.name.toString()) {
id = group.toString()
displayName = "ghooks"
description = "Simple Git hooks"
implementationClass = "com.gtramontina.ghooks.GHooks"
}
}
}

pluginBundle {
website = "https://github.com/gtramontina/ghooks.gradle"
vcsUrl = "https://github.com/gtramontina/ghooks.gradle"
tags = ["gradle", "plugin", "git", "hook", "ghook", "ghooks", "versioned"]
}

// Plugin Publishing Custom Tasks --------------------------------------------------------------------------------------

tasks.create("publish") {
group = "plugin portal"
description = "Alias for 'publishPlugins'"
dependsOn("publishPlugins")
}

tasks.create("checkPublishCredentials") {
group = "plugin portal"
description = "Checks if your environment has the publishing credentials properly setup."

def key = getProperty("gradle.publish.key", getenv("GRADLE_PUBLISH_KEY"))
def secret = getProperty("gradle.publish.secret", getenv("GRADLE_PUBLISH_SECRET"))
doLast {
if (!key?.trim()) throw new GradleException("Could not find either the system property 'gradle.publish.key' or the environment variable 'GRADLE_PUBLISH_KEY'")
if (!secret?.trim()) throw new GradleException("Could not find either the system property 'gradle.publish.secret' or the environment variable 'GRADLE_PUBLISH_SECRET'")
setProperty("gradle.publish.key", key)
setProperty("gradle.publish.secret", secret)
}
}

tasks.withType(LoginTask) { enabled = false }
tasks.withType(PublishTask) { dependsOn("checkPublishCredentials") }
61 changes: 0 additions & 61 deletions build.gradle.kts

This file was deleted.

1 change: 1 addition & 0 deletions package.json
@@ -1,4 +1,5 @@
{
"repository": "https://github.com/gtramontina/ghooks.gradle.git",
"devDependencies": {
"@semantic-release/changelog": "3.0.2",
"@semantic-release/commit-analyzer": "6.1.0",
Expand Down
65 changes: 65 additions & 0 deletions src/main/kotlin/com/gtramontina/ghooks/GHooks.kt
@@ -0,0 +1,65 @@
package com.gtramontina.ghooks

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.internal.impldep.org.apache.commons.io.FileUtils.forceDelete
import java.nio.file.Files.createSymbolicLink
import java.nio.file.Files.exists
import java.nio.file.Files.isDirectory
import java.nio.file.Files.isSymbolicLink
import java.nio.file.Path

class GHooks : Plugin<Project> {
companion object {
private const val GIT_HOOKS_TARGET = ".githooks"
private const val GIT_HOOKS_SOURCE = ".git/hooks"
private const val DESCRIPTION = "Installs the configured Git hooks."
private const val GROUP = "git hooks"
private const val TASK_NAME = "installGitHooks"
private const val GIT_CDUP = "git rev-parse --show-cdup"
private const val GIT_HOOKS_TARGET_WARNING = """
Something went wrong while installing your Git hooks.
Please make sure you have the `$GIT_HOOKS_TARGET` directory on the root of your project.
Once these conditions are satisfied, this plugin will ensure the hooks get installed.
"""
private const val GIT_WARNING = """
Something went wrong while installing your Git hooks.
Please make sure you have `git` installed and that this is a Git repository.
Once these conditions are satisfied, this plugin will ensure the hooks get installed.
"""
}

override fun apply(project: Project) {
project.task(TASK_NAME) { task ->
task.description = DESCRIPTION
task.group = GROUP

val root = project.rootDir
val target = root.resolve(GIT_HOOKS_TARGET).toPath()

if (!isDirectory(target))
task.logger.warn(GIT_HOOKS_TARGET_WARNING.trimIndent()).also { return@task }

GIT_CDUP.exec(root)
.thenApply { root.resolve(it.trim()) }
.thenApply { it.resolve(GIT_HOOKS_SOURCE).toPath() }
.thenAccept { it.symLinkTo(target) }
.exceptionally { task.logger.warn(GIT_WARNING.trimIndent(), it).run { throw it } }
}
}

private fun Path.symLinkTo(target: Path) {
if (isSymLinkTo(target)) return
deleteIfExists().also { createSymbolicLink(this, target) }
}

private fun Path.isSymLinkTo(target: Path) = exists(this)
.and(isSymbolicLink(this))
.and(toRealPath() == target.toRealPath())

private fun Path.deleteIfExists() = forceDelete(toFile())
}
10 changes: 0 additions & 10 deletions src/main/kotlin/com/gtramontina/ghooks/Main.kt

This file was deleted.

24 changes: 24 additions & 0 deletions src/main/kotlin/com/gtramontina/ghooks/String.exec.kt
@@ -0,0 +1,24 @@
package com.gtramontina.ghooks

import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.supplyAsync
import java.util.concurrent.TimeUnit.SECONDS

private const val SUCCESSFUL = 0
private val BLANK_SPACES = "\\s".toRegex()

internal fun String.exec(cwd: File, timeoutInSeconds: Long = 60): CompletableFuture<String> = supplyAsync {
ProcessBuilder(*split(BLANK_SPACES).toTypedArray())
.redirectErrorStream(true)
.directory(cwd)
.start()
.apply { waitFor(timeoutInSeconds, SECONDS) }
.let { it.exitValue() to it.inputStream.bufferedReader().readText() }
.let { (code, output) ->
when (code) {
SUCCESSFUL -> output
else -> throw Error(output)
}
}
}
53 changes: 53 additions & 0 deletions src/test/kotlin/com/gtramontina/ghooks/Inhospitable Environment.kt
@@ -0,0 +1,53 @@
package com.gtramontina.ghooks

import com.gtramontina.ghooks.support.TestLogger
import com.gtramontina.ghooks.support.createCustomHooks
import com.gtramontina.ghooks.support.initializeGitRepository
import com.gtramontina.ghooks.support.toRegexGI
import io.kotlintest.matchers.collections.shouldHaveSize
import io.kotlintest.matchers.string.shouldMatch
import org.awaitility.Awaitility.await
import org.awaitility.Duration.ONE_SECOND
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class `Inhospitable Environment` {
private lateinit var project: Project
private lateinit var logger: TestLogger

@BeforeEach
fun beforeEach() {
project = ProjectBuilder.builder().build()
logger = TestLogger()
}

@AfterEach
fun afterEach() = logger.reset()

@Test
fun `warns when not a git repository`() {
project.createCustomHooks(".githooks")
project.pluginManager.apply(GHooks::class.java)

await().atMost(ONE_SECOND).untilAsserted {
logger.warnings shouldHaveSize 1
logger.warnings.first() shouldMatch ".*something went wrong.*".toRegexGI()
logger.warnings.first() shouldMatch ".*that this is a git repository.*".toRegexGI()
}
}

@Test
fun `warns when the target directory does not exist`() {
project.initializeGitRepository()
project.pluginManager.apply(GHooks::class.java)

await().atMost(ONE_SECOND).untilAsserted {
logger.warnings shouldHaveSize 1
logger.warnings.first() shouldMatch ".*something went wrong.*".toRegexGI()
logger.warnings.first() shouldMatch ".*make sure you have the `.*` directory on the root of your project.*".toRegexGI()
}
}
}
42 changes: 42 additions & 0 deletions src/test/kotlin/com/gtramontina/ghooks/Standard Scenarios.kt
@@ -0,0 +1,42 @@
package com.gtramontina.ghooks

import com.gtramontina.ghooks.support.createCustomHooks
import com.gtramontina.ghooks.support.initializeGitRepository
import io.kotlintest.matchers.collections.containExactlyInAnyOrder
import io.kotlintest.should
import org.awaitility.Awaitility.await
import org.awaitility.Duration.ONE_SECOND
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.nio.file.Files.list
import java.util.stream.Collectors.toList

class `Standard Scenarios` {
private lateinit var project: Project
private var customHooks = listOf("pre-commit", "prepare-commit-msg")

@BeforeEach
fun `setup project as a git repository`() {
project = ProjectBuilder.builder().build()
project.createCustomHooks(".githooks", *customHooks.toTypedArray())
project.initializeGitRepository()
}

@Test
fun `has proper plugin ID`() {
project.pluginManager.apply("com.gtramontina.ghooks.gradle")
}

@Test
fun `installs the git hooks`() {
project.pluginManager.apply(GHooks::class.java)

await().atMost(ONE_SECOND).untilAsserted {
val ls = list(project.rootDir.resolve(".git/hooks").toPath()).collect(toList())
val installed = ls.map { it.fileName.toString() }
installed should containExactlyInAnyOrder(customHooks)
}
}
}
16 changes: 16 additions & 0 deletions src/test/kotlin/com/gtramontina/ghooks/support/Extensions.kt
@@ -0,0 +1,16 @@
package com.gtramontina.ghooks.support

import com.gtramontina.ghooks.exec
import org.gradle.api.Project
import java.nio.file.Files.createDirectories
import java.nio.file.Files.createFile
import kotlin.text.RegexOption.DOT_MATCHES_ALL
import kotlin.text.RegexOption.IGNORE_CASE
import kotlin.text.RegexOption.MULTILINE

fun String.toRegexGI() = toRegex(setOf(IGNORE_CASE, DOT_MATCHES_ALL, MULTILINE))
fun Project.initializeGitRepository(): String = "git init".exec(rootDir).get()
fun Project.createCustomHooks(dirname: String, vararg hooks: String) = rootDir
.resolve(dirname).toPath()
.let { createDirectories(it) }
.let { target -> hooks.map { target.resolve(it) }.forEach { createFile(it) } }

0 comments on commit 38b3028

Please sign in to comment.