Skip to content

Commit

Permalink
perf!: make the plugin compatible with Gradle's configuration cache (#…
Browse files Browse the repository at this point in the history
…677)

* build(kotlin): enable context receivers via compiler args

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>

* feat: make GitSemVer configuration cache compatible

Use ValueSource APIs for running external processes in a cacheable way

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>

* feat!: lift project from extension for configuration cache compatibility

Referring to project instance from task action is a bad practice in general and a compilation error with configuration cache. By lifting project out it's possible to pass only actually needed values like projectDir and logger.
This change also highlighted the fact project version is getting mutated inside the extension in a side effect-like way.

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>

* Revert "build(kotlin): enable context receivers via compiler args"

This reverts commit 00fa181.

---------

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>
  • Loading branch information
maksym-moroz authored Jan 21, 2024
1 parent 8505839 commit 75396a7
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 114 deletions.
62 changes: 36 additions & 26 deletions src/main/kotlin/org/danilopianini/gradle/gitsemver/GitSemVer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,48 @@ package org.danilopianini.gradle.gitsemver

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ProviderFactory
import javax.inject.Inject

/**
* A Plugin for comuting the project version based on the status of the local git repository.
*/
class GitSemVer : Plugin<Project> {
class GitSemVer @Inject constructor(
private val providerFactory: ProviderFactory,
private val objectFactory: ObjectFactory,
) : Plugin<Project> {

override fun apply(project: Project) {
with(project) {
/*
* Recursively scan project directory. If git repo is found, rely on GitSemVerExtension to inspect it.
*/
val extension = project.createExtension<GitSemVerExtension>(GitSemVerExtension.EXTENSION_NAME, project)
project.afterEvaluate {
with(extension) {
properties[extension.forceVersionPropertyName.get()]?.let {
require(SemanticVersion.semVerRegex.matches(it.toString())) {
"The version '$it' is not a valid semantic versioning format"
}
project.logger.lifecycle(
"Forcing version to $it, mandated by property '$forceVersionPropertyName'",
)
project.version = it.toString()
} ?: run { assignGitSemanticVersion() }
}
}
tasks.register("printGitSemVer") {
it.doLast {
println(
"Version computed by ${GitSemVer::class.java.simpleName}: " +
"${properties[extension.forceVersionPropertyName.get()] ?: extension.computeVersion()}",
)
override fun apply(project: Project): Unit = with(project) {
/*
* Recursively scan project directory. If git repo is found, rely on GitSemVerExtension to inspect it.
*/
val extension = createExtension<GitSemVerExtension>(
GitSemVerExtension.EXTENSION_NAME,
providerFactory,
objectFactory,
projectDir,
version,
logger,
)
afterEvaluate {
properties[extension.forceVersionPropertyName.get()]?.let { forceVersion ->
require(SemanticVersion.semVerRegex.matches(forceVersion.toString())) {
"The version '$forceVersion. is not a valid semantic versioning format"
}
logger.lifecycle(
"Forcing version to $forceVersion. mandated by property '$extension.forceVersionPropertyName'",
)
version = forceVersion
} ?: run { version = extension.assignGitSemanticVersion() }
}
tasks.register("printGitSemVer") {
val forceVersion = properties[extension.forceVersionPropertyName.get()]
it.doLast {
println(
"Version computed by ${GitSemVer::class.java.simpleName}: " +
"${forceVersion ?: extension.computeVersion()}",
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.danilopianini.gradle.gitsemver

import org.gradle.api.Project
import org.danilopianini.gradle.gitsemver.source.GitCommandValueSource
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.slf4j.Logger
import java.io.File

Expand Down Expand Up @@ -31,23 +34,26 @@ import java.io.File
* version. By default the property name is "forceVersion".
*/
open class GitSemVerExtension @JvmOverloads constructor(
private val project: Project,
val minimumVersion: Property<String> = project.propertyWithDefault("0.1.0"),
val developmentIdentifier: Property<String> = project.propertyWithDefault("dev"),
val noTagIdentifier: Property<String> = project.propertyWithDefault("archeo"),
val fullHash: Property<Boolean> = project.propertyWithDefault(false),
val maxVersionLength: Property<Int> = project.propertyWithDefault(Int.MAX_VALUE),
val developmentCounterLength: Property<Int> = project.propertyWithDefault(2),
val enforceSemanticVersioning: Property<Boolean> = project.propertyWithDefault(true),
val preReleaseSeparator: Property<String> = project.propertyWithDefault("-"),
val buildMetadataSeparator: Property<String> = project.propertyWithDefault("+"),
val distanceCounterRadix: Property<Int> = project.propertyWithDefault(DEFAULT_RADIX),
val versionPrefix: Property<String> = project.propertyWithDefault(""),
val includeLightweightTags: Property<Boolean> = project.propertyWithDefault(true),
val forceVersionPropertyName: Property<String> = project.propertyWithDefault("forceVersion"),
private val providerFactory: ProviderFactory,
private val objectFactory: ObjectFactory,
private val projectDir: File,
private val version: String,
private val logger: Logger,
val minimumVersion: Property<String> = objectFactory.propertyWithDefault("0.1.0"),
val developmentIdentifier: Property<String> = objectFactory.propertyWithDefault("dev"),
val noTagIdentifier: Property<String> = objectFactory.propertyWithDefault("archeo"),
val fullHash: Property<Boolean> = objectFactory.propertyWithDefault(false),
val maxVersionLength: Property<Int> = objectFactory.propertyWithDefault(Int.MAX_VALUE),
val developmentCounterLength: Property<Int> = objectFactory.propertyWithDefault(2),
val enforceSemanticVersioning: Property<Boolean> = objectFactory.propertyWithDefault(true),
val preReleaseSeparator: Property<String> = objectFactory.propertyWithDefault("-"),
val buildMetadataSeparator: Property<String> = objectFactory.propertyWithDefault("+"),
val distanceCounterRadix: Property<Int> = objectFactory.propertyWithDefault(DEFAULT_RADIX),
val versionPrefix: Property<String> = objectFactory.propertyWithDefault(""),
val includeLightweightTags: Property<Boolean> = objectFactory.propertyWithDefault(true),
val forceVersionPropertyName: Property<String> = objectFactory.propertyWithDefault("forceVersion"),
private var updateStrategy: (List<String>) -> UpdateType = { _ -> UpdateType.PATCH },
) {

/**
* Sets the strategy to be used to compute the version increment based on the commit messages since the last tag.
* The default strategy is to increment the patch version.
Expand All @@ -58,7 +64,7 @@ open class GitSemVerExtension @JvmOverloads constructor(
updateStrategy = strategy
}

private fun computeMinVersion(logger: Logger): SemanticVersion {
private fun computeMinVersion(): SemanticVersion {
val minVersion = minimumVersion.get()
val minSemVer = SemanticVersion.fromStringOrNull(minVersion)?.withoutBuildMetadata()
requireNotNull(minSemVer) {
Expand All @@ -73,7 +79,7 @@ open class GitSemVerExtension @JvmOverloads constructor(
/**
* Finds the closest tag compatible with Semantic Version, or returns null if none is available.
*/
fun Project.findClosestTag(): SemanticVersion? {
fun findClosestTag(): SemanticVersion? {
val reachableCommits = runCommand("git", "rev-list", "HEAD")?.lines()?.toSet().orEmpty()
val tagMatcher = Regex(
"""^(\w*)\s+(${
Expand All @@ -88,7 +94,7 @@ open class GitSemVerExtension @JvmOverloads constructor(
return runCommand("git", "for-each-ref", "refs/tags", "--sort=-version:refname")
?.lineSequence()
?.mapNotNull { tagMatcher.matchEntire(it)?.destructured }
?.mapNotNull { (commit, type, semVer, major, minor, patch, option, build) ->
?.mapNotNull { (commit, type: String, semVer, major, minor, patch, option, build) ->
val actualRef = when (type) {
"commit" -> commit
"tag" -> runCommand("git", "rev-list", "-n1", versionPrefix.get() + semVer)
Expand All @@ -105,58 +111,55 @@ open class GitSemVerExtension @JvmOverloads constructor(
* Computes a valid Semantic Versioning 2.0 version based on the status of the current git repository.
*/
fun computeVersion(): String {
with(project) {
val closestTag = findClosestTag()
logger.debug("Closest SemVer tag: $closestTag")
val fullHash = fullHash.get()
val printCommitCommand = "git rev-parse ${if (fullHash) "" else "--short "}HEAD".split(" ")
val hash = runCommand(*printCommitCommand.toTypedArray())
?: System.currentTimeMillis().toString()
return when (closestTag) {
null -> {
val base = computeMinVersion(logger)
val identifier = noTagIdentifier.orElse("").get()
val separator = if (identifier.isBlank()) "" else preReleaseSeparator.get()
return "$base$separator$identifier${buildMetadataSeparator.get()}$hash"
}
val closestTag = findClosestTag()
logger.debug("Closest SemVer tag: $closestTag")
val fullHash = fullHash.get()
val printCommitCommand = "git rev-parse ${if (fullHash) "" else "--short "}HEAD".split(" ")
val hash = runCommand(*printCommitCommand.toTypedArray()) ?: System.currentTimeMillis().toString()
return when (closestTag) {
null -> {
val base = computeMinVersion()
val identifier = noTagIdentifier.orElse("").get()
val separator = if (identifier.isBlank()) "" else preReleaseSeparator.get()
return "$base$separator$identifier${buildMetadataSeparator.get()}$hash"
}

else -> {
if (!closestTag.buildMetadata.isEmpty()) {
logger.warn("Build metadata of closest tag $closestTag will be ignored.")
}
val distance = runCommand(
"git",
"rev-list",
"--count",
"${versionPrefix.get()}$closestTag..HEAD",
)?.toLong()
requireNotNull(distance) {
"Bug in git SemVer plugin: [distance? $distance]. Please report at: " +
"https://github.com/DanySK/git-sensitive-semantic-versioning-gradle-plugin/issues"
}
when (distance) {
0L -> closestTag.toString()
else -> {
val base: SemanticVersion = closestTag.withoutBuildMetadata()
val lastCommits = runCommand(
"git",
"log",
"--oneline",
"-$distance",
"--no-decorate",
"--format=%s",
)?.lines().orEmpty()
val currentVersion = updateStrategy(lastCommits).incrementVersion(base)
val devString = developmentIdentifier.get()
val separator = if (devString.isBlank()) "" else preReleaseSeparator.get()
val distanceString = distance.withRadix(
distanceCounterRadix.get(),
developmentCounterLength.get(),
)
val buildSeparator = buildMetadataSeparator.get()
"$currentVersion$separator$devString$distanceString$buildSeparator$hash"
.take(maxVersionLength.get())
}
else -> {
if (!closestTag.buildMetadata.isEmpty()) {
logger.warn("Build metadata of closest tag $closestTag will be ignored.")
}
val distance = runCommand(
"git",
"rev-list",
"--count",
"${versionPrefix.get()}$closestTag..HEAD",
)?.toLong()
requireNotNull(distance) {
"Bug in git SemVer plugin: [distance? $distance]. Please report at: " +
"https://github.com/DanySK/git-sensitive-semantic-versioning-gradle-plugin/issues"
}
when (distance) {
0L -> closestTag.toString()
else -> {
val base: SemanticVersion = closestTag.withoutBuildMetadata()
val lastCommits = runCommand(
"git",
"log",
"--oneline",
"-$distance",
"--no-decorate",
"--format=%s",
)?.lines().orEmpty()
val currentVersion = updateStrategy(lastCommits).incrementVersion(base)
val devString = developmentIdentifier.get()
val separator = if (devString.isBlank()) "" else preReleaseSeparator.get()
val distanceString = distance.withRadix(
distanceCounterRadix.get(),
developmentCounterLength.get(),
)
val buildSeparator = buildMetadataSeparator.get()
"$currentVersion$separator$devString$distanceString$buildSeparator$hash"
.take(maxVersionLength.get())
}
}
}
Expand All @@ -166,19 +169,19 @@ open class GitSemVerExtension @JvmOverloads constructor(
/**
* modifies the version of the current project, assigning the value computed by [computeVersion].
*/
fun assignGitSemanticVersion() {
fun assignGitSemanticVersion(): String {
val computedVersion = computeVersion()
val resultingVersion = SemanticVersion.fromStringOrNull(computedVersion)
if (resultingVersion == null) {
val error = "Invalid Semantic Versioning 2.0 version: ${project.version}"
return if (resultingVersion == null) {
val error = "Invalid Semantic Versioning 2.0 version: $version"
if (enforceSemanticVersioning.get()) {
error(error)
} else {
project.logger.warn(error)
logger.warn(error)
}
project.version = computedVersion
computedVersion
} else {
project.version = resultingVersion.toString()
resultingVersion.toString()
}
}

Expand All @@ -189,6 +192,21 @@ open class GitSemVerExtension @JvmOverloads constructor(
includeLightweightTags.set(false)
}

private fun runCommand(vararg cmd: String) = processCommand(*cmd)

private fun processCommand(vararg cmd: String) = createValueSourceProvider(*cmd)
.get()
.trim()
.takeIf { it.isNotEmpty() }

private fun createValueSourceProvider(vararg cmd: String): Provider<String> =
providerFactory.of(GitCommandValueSource::class.java) {
it.parameters { params ->
params.commands.set(objectFactory.listProperty(String::class.java).value(cmd.asList()))
params.directory.set(projectDir)
}
}

companion object {

/**
Expand All @@ -198,18 +216,8 @@ open class GitSemVerExtension @JvmOverloads constructor(

private const val DEFAULT_RADIX = 36

private inline fun <reified T> Project.propertyWithDefault(default: T): Property<T> =
objects.property(T::class.java).apply { convention(default) }

private fun Project.runCommand(vararg cmd: String) = projectDir.runCommandInFolder(*cmd)

private fun File.runCommandInFolder(vararg cmd: String) = Runtime.getRuntime()
.exec(cmd, emptyArray(), this)
.inputStream
.bufferedReader()
.readText()
.trim()
.takeIf { it.isNotEmpty() }
private inline fun <reified T> ObjectFactory.propertyWithDefault(default: T): Property<T> =
property(T::class.java).apply { convention(default) }

private fun Long.withRadix(radix: Int, digits: Int? = null) = toString(radix).let {
if (digits == null || it.length >= digits) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.danilopianini.gradle.gitsemver.source

import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.charset.Charset
import javax.inject.Inject

/**
* Value source for reading results of external git commands.
*/
abstract class GitCommandValueSource : ValueSource<String, Parameters> {

/**
* Execution operations instance to execute external process.
*/
@get:Inject
abstract val execOperations: ExecOperations

override fun obtain(): String {
val output = ByteArrayOutputStream()

execOperations.exec {
it.apply {
commandLine = parameters.commands.get()
workingDir = parameters.directory.get()
standardOutput = output
isIgnoreExitValue = true
}
}
return String(output.toByteArray(), Charset.defaultCharset())
}
}

/**
* Parameters for passing down git command list.
*/
interface Parameters : ValueSourceParameters {

/**
* List of commands to execute in an external process.
*/
val commands: ListProperty<String>

/**
* Working directory to execute external process in.
*/
val directory: Property<File>
}

0 comments on commit 75396a7

Please sign in to comment.