From b69279ca8c99abc52e5de6cfd03b38ca024471e2 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Mon, 8 Sep 2025 16:22:41 +0200 Subject: [PATCH 1/3] add git extension --- README.md | 4 + build.gradle.kts | 4 + src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt | 9 +- .../ltd/lulz/plugin/extension/GitExtension.kt | 37 ++++ .../lulz/plugin/extension/GitExtensionTest.kt | 162 ++++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/ltd/lulz/plugin/extension/GitExtension.kt create mode 100644 src/test/kotlin/ltd/lulz/plugin/extension/GitExtensionTest.kt diff --git a/README.md b/README.md index 5e8027a..8197edd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ ## Extension +### Extension Git + +The GitExtension enhances versioning by dynamically appending the Git hash before "snapshot" in the version string. For example, 0.0.0-SNAPSHOT becomes 0.0.0.0a2b3c4d-SNAPSHOT, ensuring each build reflects its commit origin, prevents overwriting existing versions. This feature aids developers during development by providing clear version identification. + ## Publish version catalog locally. ```shell diff --git a/build.gradle.kts b/build.gradle.kts index 9a0f3f3..ad2a306 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,11 @@ plugins { } dependencies { + implementation(aa.jgit) + + testImplementation(aa.junit.jupiter.params) testImplementation(aa.kotlin.junit5) + testImplementation(aa.mockk) testRuntimeOnly(aa.junit.platform.launcher) } diff --git a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt index 198fcd6..e757d57 100644 --- a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt +++ b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt @@ -1,7 +1,9 @@ package ltd.lulz.plugin +import ltd.lulz.plugin.extension.GitExtension import org.gradle.api.Plugin import org.gradle.api.Project +import ltd.lulz.plugin.extension.GitExtension.Companion.PLUGIN_NAME as GIT_PLUGIN_NAME @Suppress("unused") class CorePlugin : Plugin { @@ -12,6 +14,11 @@ class CorePlugin : Plugin { override fun apply( project: Project, ) { - TODO("Not yet implemented") + gitExtension(project) } + + private fun gitExtension( + project: Project, + ): GitExtension = project.extensions + .create(GIT_PLUGIN_NAME, GitExtension::class.java, project) } diff --git a/src/main/kotlin/ltd/lulz/plugin/extension/GitExtension.kt b/src/main/kotlin/ltd/lulz/plugin/extension/GitExtension.kt new file mode 100644 index 0000000..a5655a8 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/plugin/extension/GitExtension.kt @@ -0,0 +1,37 @@ +package ltd.lulz.plugin.extension + +import org.eclipse.jgit.api.Git.open +import org.eclipse.jgit.lib.Constants.HEAD +import org.gradle.api.Project + +abstract class GitExtension(private val project: Project) { + + companion object { + const val PLUGIN_NAME = "git" + + private const val HASH_LENGTH = 8 + private const val SNAPSHOT = "-SNAPSHOT" + private const val UNAVAILABLE = "n/a" + private val PRIMARY_BRANCHES = setOf("master", "develop") + } + + fun version(): String = when { + isHead() || currentBranch() in PRIMARY_BRANCHES -> project.version.toString() + else -> makeVersion(project.version.toString(), currentShortHash()) + } + + fun currentShortHash(): String = open(project.projectDir) + .use { it.repository.exactRef(HEAD)?.objectId?.name?.take(HASH_LENGTH) ?: UNAVAILABLE } + + fun currentBranch(): String = open(project.projectDir) + .use { it.repository.branch ?: UNAVAILABLE } + + fun isHead(): Boolean = open(project.projectDir) + .use { it.repository.exactRef(HEAD)?.target?.name == HEAD } + + private fun makeVersion(version: String, shortHash: String): String = when { + shortHash == UNAVAILABLE -> version.also { println("Failed to get data from GIT") } + version.endsWith(SNAPSHOT) -> version.replace(SNAPSHOT, ".$shortHash$SNAPSHOT") + else -> version.also { println("Failed version missing suffix $SNAPSHOT") } + } +} diff --git a/src/test/kotlin/ltd/lulz/plugin/extension/GitExtensionTest.kt b/src/test/kotlin/ltd/lulz/plugin/extension/GitExtensionTest.kt new file mode 100644 index 0000000..96f569b --- /dev/null +++ b/src/test/kotlin/ltd/lulz/plugin/extension/GitExtensionTest.kt @@ -0,0 +1,162 @@ +package ltd.lulz.plugin.extension + +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.lib.Repository +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class GitExtensionTest { + companion object { + const val GIT_HASH_SHA_1 = "0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t" + const val SHORT_GIT_HASH = "0a1b2c3d" + const val SNAPSHOT_HASH_VERSION = "0.0.0.0a1b2c3d-SNAPSHOT" + const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT" + const val VERSION = "0.0.0" + + const val BRANCH_FEATURE = "feature/ABC-123" + const val BRANCH_MASTER = "master" + const val EXTENSION = "git" + const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin" + const val REF_HEAD = "HEAD" + const val UNAVAILABLE = "n/a" + } + + private val gitMock: Git = mockk() + private val refMock: Ref = mockk() + private val repositoryMock: Repository = mockk() + private val objectIdMock: ObjectId = mockk() + + lateinit var name: String + lateinit var project: Project + + @BeforeEach + fun buildUp() { + project = ProjectBuilder.builder().build() + project.version = SNAPSHOT_VERSION + project.pluginManager.apply(PLUGIN_ID) + mockkStatic(Git::class) + + every { Git.open(any()) } returns gitMock + every { gitMock.repository } returns repositoryMock + every { repositoryMock.branch } returns null + every { repositoryMock.exactRef(any()) } returns refMock + every { refMock.target } returns refMock + every { refMock.name } returns null + every { refMock.objectId } returns objectIdMock + every { objectIdMock.name } returns null + + justRun { gitMock.close() } + } + + @AfterEach + fun tearDown() { + unmockkStatic(Git::class) + } + + @ParameterizedTest + @CsvSource( + "$REF_HEAD, true", + "$BRANCH_FEATURE, false", + ) + fun `current ref is head`(ref: String, expected: Boolean) { + // given + every { refMock.name } returns ref + + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(expected, extension.isHead()) + } + + @ParameterizedTest + @CsvSource( + ", $UNAVAILABLE", + "$BRANCH_FEATURE, $BRANCH_FEATURE", + ) + fun `get current branch`(branch: String?, expected: String) { + // given + every { repositoryMock.branch } returns branch + + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(expected, extension.currentBranch()) + } + + @ParameterizedTest + @CsvSource( + ", $UNAVAILABLE", + "$GIT_HASH_SHA_1, $SHORT_GIT_HASH", + ) + fun `get current short hash`(hash: String?, expected: String) { + // given + every { objectIdMock.name } returns hash + + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(expected, extension.currentShortHash()) + } + + @Test + fun `get version - version without snapshot`() { + // given + project.version = VERSION + every { objectIdMock.name } returns GIT_HASH_SHA_1 + + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(VERSION, extension.version()) + } + + @Test + fun `get version - short hash is blank`() { + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(SNAPSHOT_VERSION, extension.version()) + } + + @ParameterizedTest + @CsvSource( + ", , $SNAPSHOT_HASH_VERSION", + ", $REF_HEAD, $SNAPSHOT_VERSION", + "$BRANCH_MASTER, , $SNAPSHOT_VERSION", + "$BRANCH_MASTER, , $SNAPSHOT_VERSION", + "$BRANCH_FEATURE, , $SNAPSHOT_HASH_VERSION", + "$BRANCH_MASTER, $REF_HEAD, $SNAPSHOT_VERSION", + "$BRANCH_MASTER, $REF_HEAD, $SNAPSHOT_VERSION", + "$BRANCH_FEATURE, $REF_HEAD, $SNAPSHOT_VERSION", + ) + fun `get version - different branches`(branch: String?, ref: String?, expected: String) { + // given + every { repositoryMock.branch } returns branch + every { objectIdMock.name } returns GIT_HASH_SHA_1 + every { refMock.name } returns ref + + // when + val extension = project.extensions.getByName(EXTENSION) as GitExtension + + // then + assertEquals(expected, extension.version()) + } +} -- 2.49.1 From 56ed67567d89734883e86c58034ea5145072f782 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Mon, 8 Sep 2025 16:55:07 +0200 Subject: [PATCH 2/3] add info extension --- README.md | 4 + src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt | 8 ++ .../lulz/plugin/extension/InfoExtension.kt | 16 ++++ .../plugin/extension/InfoExtensionTest.kt | 73 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/main/kotlin/ltd/lulz/plugin/extension/InfoExtension.kt create mode 100644 src/test/kotlin/ltd/lulz/plugin/extension/InfoExtensionTest.kt diff --git a/README.md b/README.md index 8197edd..d6ac4d4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ The GitExtension enhances versioning by dynamically appending the Git hash before "snapshot" in the version string. For example, 0.0.0-SNAPSHOT becomes 0.0.0.0a2b3c4d-SNAPSHOT, ensuring each build reflects its commit origin, prevents overwriting existing versions. This feature aids developers during development by providing clear version identification. +### Extension Info + +The InfoExtension provides information for name and version, vendor name, and UTC timestamp. + ## Publish version catalog locally. ```shell diff --git a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt index e757d57..00eaa36 100644 --- a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt +++ b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt @@ -1,9 +1,11 @@ package ltd.lulz.plugin import ltd.lulz.plugin.extension.GitExtension +import ltd.lulz.plugin.extension.InfoExtension import org.gradle.api.Plugin import org.gradle.api.Project import ltd.lulz.plugin.extension.GitExtension.Companion.PLUGIN_NAME as GIT_PLUGIN_NAME +import ltd.lulz.plugin.extension.InfoExtension.Companion.PLUGIN_NAME as INFO_PLUGIN_NAME @Suppress("unused") class CorePlugin : Plugin { @@ -15,8 +17,14 @@ class CorePlugin : Plugin { project: Project, ) { gitExtension(project) + infoExtension(project) } + private fun infoExtension( + project: Project, + ): InfoExtension = project.extensions + .create(INFO_PLUGIN_NAME, InfoExtension::class.java, project) + private fun gitExtension( project: Project, ): GitExtension = project.extensions diff --git a/src/main/kotlin/ltd/lulz/plugin/extension/InfoExtension.kt b/src/main/kotlin/ltd/lulz/plugin/extension/InfoExtension.kt new file mode 100644 index 0000000..6d6ba33 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/plugin/extension/InfoExtension.kt @@ -0,0 +1,16 @@ +package ltd.lulz.plugin.extension + +import java.time.OffsetDateTime.now +import java.time.ZoneId.of +import java.time.format.DateTimeFormatter.ofPattern +import org.gradle.api.Project + +abstract class InfoExtension(private val project: Project) { + companion object { + const val PLUGIN_NAME = "info" + } + + val nameVersion get() = "Project Name: ${project.name} Version: ${project.version}" + val utcTimestamp = now().atZoneSameInstant(of("UTC")).format(ofPattern("yyyy-MM-dd HH:mm:ss z")).toString() + val vendorName = "Lulz Ltd" +} diff --git a/src/test/kotlin/ltd/lulz/plugin/extension/InfoExtensionTest.kt b/src/test/kotlin/ltd/lulz/plugin/extension/InfoExtensionTest.kt new file mode 100644 index 0000000..3525be7 --- /dev/null +++ b/src/test/kotlin/ltd/lulz/plugin/extension/InfoExtensionTest.kt @@ -0,0 +1,73 @@ +package ltd.lulz.plugin.extension + +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import java.time.OffsetDateTime +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class InfoExtensionTest { + + companion object { + const val BASIC_TIMESTAMP = "2002-02-20T02:10:11+01:00" + const val EXTENSION = "info" + const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin" + const val PROJECT_NAME = "test-project" + const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT" + + @JvmStatic + @BeforeAll + fun buildUp() { + mockkStatic(OffsetDateTime::class) + every { OffsetDateTime.now() } returns OffsetDateTime.parse(BASIC_TIMESTAMP) + } + + @JvmStatic + @AfterAll + fun tearDown() { + unmockkStatic(OffsetDateTime::class) + } + } + + lateinit var project: Project + + @BeforeEach + fun beforeEach() { + project = ProjectBuilder.builder().withName(PROJECT_NAME).build() + project.version = SNAPSHOT_VERSION + project.pluginManager.apply(PLUGIN_ID) + } + + @Test + fun `get vendor`() { + // when + val extension = project.extensions.getByName(EXTENSION) as InfoExtension + + // then + assertEquals("Lulz Ltd", extension.vendorName) + } + + @Test + fun `get timestamp`() { + // when + val extension = project.extensions.getByName(EXTENSION) as InfoExtension + + // then + assertEquals("2002-02-20 01:10:11 UTC", extension.utcTimestamp) + } + + @Test + fun `get build`() { + // when + val extension = project.extensions.getByName(EXTENSION) as InfoExtension + + // then + assertEquals("Project Name: test-project Version: 0.0.0-SNAPSHOT", extension.nameVersion) + } +} -- 2.49.1 From 8dd7dc7d4d28db60d88a7eaaec11a2070127ab32 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Mon, 8 Sep 2025 17:15:43 +0200 Subject: [PATCH 3/3] add info extension --- README.md | 4 + build.gradle.kts | 3 + src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt | 8 ++ .../lulz/plugin/extension/ConfigExtension.kt | 31 +++++ .../plugin/extension/ConfigExtensionTest.kt | 112 ++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 src/main/kotlin/ltd/lulz/plugin/extension/ConfigExtension.kt create mode 100644 src/test/kotlin/ltd/lulz/plugin/extension/ConfigExtensionTest.kt diff --git a/README.md b/README.md index d6ac4d4..b789be0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ The GitExtension enhances versioning by dynamically appending the Git hash befor The InfoExtension provides information for name and version, vendor name, and UTC timestamp. +### Extension Config + +The ConfigExtension provides a find or findOrDefault for getting a property or environment. + ## Publish version catalog locally. ```shell diff --git a/build.gradle.kts b/build.gradle.kts index ad2a306..9c28767 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ plugins { dependencies { implementation(aa.jgit) + testImplementation(aa.assertj) testImplementation(aa.junit.jupiter.params) testImplementation(aa.kotlin.junit5) testImplementation(aa.mockk) @@ -114,6 +115,8 @@ tasks { } } withType { + // Set TEST_ENV environment variable for test execution + environment["TEST_ENV"] = "lulz" useJUnitPlatform() } } diff --git a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt index 00eaa36..a24fa80 100644 --- a/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt +++ b/src/main/kotlin/ltd/lulz/plugin/CorePlugin.kt @@ -1,9 +1,11 @@ package ltd.lulz.plugin +import ltd.lulz.plugin.extension.ConfigExtension import ltd.lulz.plugin.extension.GitExtension import ltd.lulz.plugin.extension.InfoExtension import org.gradle.api.Plugin import org.gradle.api.Project +import ltd.lulz.plugin.extension.ConfigExtension.Companion.PLUGIN_NAME as CONFIG_PLUGIN_NAME import ltd.lulz.plugin.extension.GitExtension.Companion.PLUGIN_NAME as GIT_PLUGIN_NAME import ltd.lulz.plugin.extension.InfoExtension.Companion.PLUGIN_NAME as INFO_PLUGIN_NAME @@ -18,8 +20,14 @@ class CorePlugin : Plugin { ) { gitExtension(project) infoExtension(project) + configExtension(project) } + private fun configExtension( + project: Project, + ): ConfigExtension = project.extensions + .create(CONFIG_PLUGIN_NAME, ConfigExtension::class.java, project) + private fun infoExtension( project: Project, ): InfoExtension = project.extensions diff --git a/src/main/kotlin/ltd/lulz/plugin/extension/ConfigExtension.kt b/src/main/kotlin/ltd/lulz/plugin/extension/ConfigExtension.kt new file mode 100644 index 0000000..dc23fa0 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/plugin/extension/ConfigExtension.kt @@ -0,0 +1,31 @@ +package ltd.lulz.plugin.extension + +import java.lang.System.getenv +import org.gradle.api.Project + +abstract class ConfigExtension(private val project: Project) { + + companion object { + const val PLUGIN_NAME = "config" + const val EMPTY = "" + } + + fun find( + property: String, + environment: String, + ): String? = findProperty(property) ?: findEnvironment(environment) + + fun findOrDefault( + property: String, + environment: String, + default: String = EMPTY, + ): String = findProperty(property) ?: findEnvironment(environment) ?: default + + private fun findProperty( + property: String, + ) = project.findProperty(property)?.toString() + + private fun findEnvironment( + environment: String, + ): String? = getenv(environment) +} diff --git a/src/test/kotlin/ltd/lulz/plugin/extension/ConfigExtensionTest.kt b/src/test/kotlin/ltd/lulz/plugin/extension/ConfigExtensionTest.kt new file mode 100644 index 0000000..c684eb7 --- /dev/null +++ b/src/test/kotlin/ltd/lulz/plugin/extension/ConfigExtensionTest.kt @@ -0,0 +1,112 @@ +package ltd.lulz.plugin.extension + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +/** + * This test uses TEST_ENV set in gradle test task, and project extension to pretend to be a project property. + */ +class ConfigExtensionTest { + + companion object { + const val EXTENSION = "config" + const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin" + const val PROJECT_NAME = "test-project" + const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT" + } + + lateinit var project: Project + + lateinit var configExtension: ConfigExtension + + @BeforeEach + fun beforeEach() { + project = ProjectBuilder.builder() + .withName(PROJECT_NAME) + .build() + project.version = SNAPSHOT_VERSION + project.pluginManager.apply(PLUGIN_ID) + project.extensions.add( + "testProperty", + object { + override fun toString(): String = "lulz" + }, + ) + configExtension = project.extensions.getByName(EXTENSION) as ConfigExtension + } + + @Nested + inner class FindTest { + + @Test + fun `property or environment find property`() { + // when + val result = configExtension.find("testProperty", "TEST_PROPERTY") + + // then + assertThat(result).isEqualTo("lulz") + } + + @Test + fun `property or environment find environment`() { + // when + val result = configExtension.find("test_env", "TEST_ENV") + + // then + assertThat(result).isEqualTo("lulz") + } + + @Test + fun `property or environment find nothing`() { + // when + val result = configExtension.find("test", "TEST") + + // then + assertThat(result).isNull() + } + } + + @Nested + inner class FindOrDefaultTest { + + @Test + fun `property, environment, or default find property`() { + // when + val result = configExtension.findOrDefault("testProperty", "TEST_PROPERTY") + + // then + assertThat(result).isEqualTo("lulz") + } + + @Test + fun `property, environment, or default find environment`() { + // when + val result = configExtension.findOrDefault("test_env", "TEST_ENV") + + // then + assertThat(result).isEqualTo("lulz") + } + + @Test + fun `property, environment, or default find empty`() { + // when + val result = configExtension.findOrDefault("test", "TEST") + + // then + assertThat(result).isEmpty() + } + + @Test + fun `property, environment, or default find defined response`() { + // when + val result = configExtension.findOrDefault("test", "TEST", "value") + + // then + assertThat(result).isEqualTo("value") + } + } +} -- 2.49.1