From 08834bb31ede30764ad94c6a77dfb97257f55143 Mon Sep 17 00:00:00 2001 From: JFronny Date: Thu, 29 Jun 2023 12:34:44 +0200 Subject: [PATCH] Enhance autoversion with auto-incrementing via commit and version parsing --- .../io/gitlab/jfronny/scripts/CommitType.kt | 28 +++++++ .../gitlab/jfronny/scripts/SemanticVersion.kt | 76 +++++++++++++++++++ .../io/gitlab/jfronny/scripts/SharedProps.kt | 8 ++ .../io/gitlab/jfronny/scripts/VersionType.kt | 19 ++++- .../src/main/kotlin/jf.autoversion.gradle.kts | 43 ++++++++--- 5 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 convention/src/main/kotlin/io/gitlab/jfronny/scripts/CommitType.kt create mode 100644 convention/src/main/kotlin/io/gitlab/jfronny/scripts/SemanticVersion.kt diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CommitType.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CommitType.kt new file mode 100644 index 0000000..8b8229d --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CommitType.kt @@ -0,0 +1,28 @@ +package io.gitlab.jfronny.scripts + +enum class CommitType { + FIX, FEAT, BREAKING; + + companion object { + private val pattern: Regex = Regex("([a-zA-Z ]+)(?:\\([a-z]+\\))?(!)?: .+", RegexOption.DOT_MATCHES_ALL) + + fun from(commitMessage: String, warn: (String) -> Unit): CommitType { + val match = pattern.matchEntire(commitMessage) + if (match != null) { + val m = match.groupValues + if (m[2] == "!") return BREAKING + return when (m[1].lowercase()) { + "fix", "build", "chore", "ci", "docs", "style", "refactor", "perf", "test" -> FIX + "feat" -> FEAT + "breaking change", "breaking" -> BREAKING + else -> { + warn("Unrecognized commit type: ${m[1]}, guessing FEAT") + FEAT + } + } + } + warn("Could not parse commit, guessing type is FEAT: $commitMessage") + return FEAT + } + } +} \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SemanticVersion.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SemanticVersion.kt new file mode 100644 index 0000000..64b5907 --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SemanticVersion.kt @@ -0,0 +1,76 @@ +package io.gitlab.jfronny.scripts + +import java.lang.Runtime.Version + +data class SemanticVersion(val major: Int, val minor: Int, val patch: Int, val type: VersionType, val build: String?): Comparable { + init { + require(build == null || buildPattern.matches(build)) { "Illegal build string" } + } + + override fun compareTo(other: SemanticVersion): Int = + when { + major > other.major -> 1 + major < other.major -> -1 + minor > other.minor -> 1 + minor < other.minor -> -1 + patch > other.patch -> 1 + patch < other.patch -> -1 + else -> when { + type != other.type -> type.compareTo(other.type) + build == null && other.build != null -> -1 + build != null && other.build == null -> 1 + build != null && other.build != null -> build.compareTo(other.build) + else -> 0 + } + } + + override fun toString(): String { + return "$major.$minor.$patch" + when (type) { + VersionType.RELEASE -> "" + else -> "-${type.semanticName}" + } + if (build == null) "" else "+$build" + } + + fun unclassifiedToString(): String { + return "$major.$minor.$patch" + if (build == null) "" else "+$build" + } + + fun incrementBy(commitType: CommitType, versionType: VersionType = VersionType.RELEASE, build: String? = null): SemanticVersion = when(commitType) { + CommitType.FIX -> SemanticVersion(major, minor, patch + 1, versionType, build) + CommitType.FEAT -> SemanticVersion(major, minor + 1, 0, versionType, build) + CommitType.BREAKING -> SemanticVersion(major + 1, 0, 0, versionType, build) + } + + fun withType(versionType: VersionType) = SemanticVersion(major, minor, patch, versionType, build) + + companion object { + private val identifier = Regex("[a-zA-Z1-9][a-zA-Z0-9]*") + private val buildPattern = Regex("$identifier(?:\\.$identifier)+") + private val number = Regex("[1-9][0-9]*") + private val versionCore = Regex("($number)\\.($number)\\.($number)") + private val legacyVersion = Regex("([vba]|rc)$versionCore(\\+$buildPattern)?") + private val restrictedSemver = Regex("$versionCore(-(?:alpha|beta|rc))?(\\+$buildPattern)?") + + fun parse(source: String): SemanticVersion { + val legacyMatch = legacyVersion.matchEntire(source) + if (legacyMatch != null) { + val m = legacyMatch.groupValues + return SemanticVersion( + m[2].toInt(), m[3].toInt(), m[4].toInt(), + VersionType.byShorthand(m[1])!!, + m[5].ifEmpty { null } + ) + } + val semverMatch = restrictedSemver.matchEntire(source) + if (semverMatch != null) { + val m = semverMatch.groupValues + return SemanticVersion( + m[1].toInt(), m[2].toInt(), m[3].toInt(), + VersionType.byName(m[4].ifEmpty { "release" })!!, + m[5].ifEmpty { null } + ) + } + throw IllegalArgumentException("Source does not match supported version patterns: $source") + } + } +} diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt index e8cad26..2151b1b 100644 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt @@ -20,6 +20,14 @@ var Project.versionType: VersionType get() = if (extra.has("versionType")) extra["versionType"] as VersionType else VersionType.RELEASE set(value) = extra.set("versionType", value) +var Project.lastRelease: String + get() = if (extra.has("lastRelease")) extra["lastRelease"].toString() else "" + set(value) = extra.set("lastRelease", value) + +var Project.nextRelease: SemanticVersion? + get() = if (extra.has("nextRelease")) extra["nextRelease"] as SemanticVersion else null + set(value) = extra.set("nextRelease", value) + var Project.changelog: String get() = if (extra.has("changelog")) extra["changelog"].toString() else "" set(value) = extra.set("changelog", value) diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/VersionType.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/VersionType.kt index 186a995..b508848 100644 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/VersionType.kt +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/VersionType.kt @@ -1,7 +1,18 @@ package io.gitlab.jfronny.scripts -enum class VersionType(val displayName: String, val curseforgeName: String, val modrinthName: String) { - RELEASE("Release", "release", "release"), - BETA("Beta", "beta", "beta"), - ALPHA("Alpha", "alpha", "alpha"); +import java.util.* +import java.util.stream.Collectors + +enum class VersionType(val displayName: String, val curseforgeName: String, val modrinthName: String, val semanticName: String, val shorthand: String): Comparable { + ALPHA("Alpha", "alpha", "alpha", "alpha", "a"), + BETA("Beta", "beta", "beta", "beta", "b"), + RELEASE_CANDIDATE("Release Candidate", "beta", "beta", "rc", "rc"), + RELEASE("Release", "release", "release", "release", "r"); + + companion object { + private val byShorthand = Arrays.stream(VersionType.values()).collect(Collectors.toUnmodifiableMap({ it.shorthand }, { it })) + private val byName = Arrays.stream(VersionType.values()).collect(Collectors.toUnmodifiableMap({ it.semanticName }, { it })) + fun byShorthand(shorthand: String): VersionType? = byShorthand[shorthand] + fun byName(name: String): VersionType? = byName[name] + } } \ No newline at end of file diff --git a/convention/src/main/kotlin/jf.autoversion.gradle.kts b/convention/src/main/kotlin/jf.autoversion.gradle.kts index 8fa0a9e..3dd38ab 100644 --- a/convention/src/main/kotlin/jf.autoversion.gradle.kts +++ b/convention/src/main/kotlin/jf.autoversion.gradle.kts @@ -2,6 +2,7 @@ import io.gitlab.jfronny.scripts.* import org.eclipse.jgit.api.Git import java.awt.Toolkit import java.awt.datatransfer.StringSelection +import kotlin.jvm.optionals.getOrDefault val isRelease = project.hasProperty("release") @@ -14,24 +15,26 @@ if (File(projectDir, ".git").exists()) { if (tags.isNotEmpty()) { if (tags[0].fullMessage != null) changelog += "${tags[0].fullMessage}\n" versionS = tags[0].name - val vt = when(versionS[0]) { - 'v' -> VersionType.RELEASE - 'b' -> VersionType.BETA - 'a' -> VersionType.ALPHA - else -> null - } - versionType = vt ?: VersionType.RELEASE - if (vt != null) versionS = versionS.substring(1) + val parsedVersion = SemanticVersion.parse(versionS) + versionType = parsedVersion.type + versionS = parsedVersion.unclassifiedToString() + lastRelease = versionS if (isRelease) { changelog += "Commits in ${versionType.displayName} $versionS:\n" changelog += git.log(if (tags.size >= 2) tags[1].peeledId else null, tags[0].peeledId) .reversed() .joinToString("\n") { "- ${it.shortMessage}" } + nextRelease = parsedVersion } else { changelog += "Commits after ${versionType.displayName} $versionS:\n" - changelog += git.log(tags[0].peeledId, git.repository.resolve("HEAD")) - .reversed() - .joinToString("\n") { "- ${it.shortMessage}" } + val log = git.log(tags[0].peeledId, git.repository.resolve("HEAD")).reversed() + changelog += log.joinToString("\n") { "- ${it.shortMessage}" } + val type = log.stream() + .map { it.fullMessage } + .map { msg -> CommitType.from(msg) { logger.warn(it) } } + .max { o1, o2 -> o1.compareTo(o2) } + .getOrDefault(CommitType.FIX) + nextRelease = parsedVersion.incrementBy(type) } } else { changelog += "Commits after inception:\n" @@ -43,14 +46,30 @@ if (File(projectDir, ".git").exists()) { } else changelog = "No changelog" if (!isRelease) { - versionS += "-SNAPSHOT" + versionS = "$nextRelease-SNAPSHOT" } println(changelog) tasks.register("copyVersionNumber") { + description = "Copy the current version number to the system clipboard" doLast { Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(versionS), null) println("Copied version number: $versionS") } } + +tasks.register("bumpVersion") { + description = "Bump the version by parsing commits since the last tag and creating a new tag based on them" + doLast { + if (!File(projectDir, ".git").exists()) throw IllegalStateException("Cannot bump without repository") + if (isRelease) throw IllegalStateException("Cannot bump while 'release' is set") + if (!project.hasProperty("versionType")) throw IllegalStateException("bumpVersion requires you to set -PversionType=release|beta|alpha") + val vt = VersionType.byName(prop("versionType")) ?: throw IllegalStateException("Unrecognized version type") + val name = nextRelease!!.withType(vt).toString() + Git.open(projectDir).use { git -> + git.tag().setName(name).call() + logger.warn("Created release $name (last was $lastRelease). Make sure to push it!") + } + } +}