Enhance autoversion with auto-incrementing via commit and version parsing
This commit is contained in:
parent
cd251164e9
commit
08834bb31e
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SemanticVersion> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,14 @@ var Project.versionType: VersionType
|
||||||
get() = if (extra.has("versionType")) extra["versionType"] as VersionType else VersionType.RELEASE
|
get() = if (extra.has("versionType")) extra["versionType"] as VersionType else VersionType.RELEASE
|
||||||
set(value) = extra.set("versionType", value)
|
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
|
var Project.changelog: String
|
||||||
get() = if (extra.has("changelog")) extra["changelog"].toString() else ""
|
get() = if (extra.has("changelog")) extra["changelog"].toString() else ""
|
||||||
set(value) = extra.set("changelog", value)
|
set(value) = extra.set("changelog", value)
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
package io.gitlab.jfronny.scripts
|
package io.gitlab.jfronny.scripts
|
||||||
|
|
||||||
enum class VersionType(val displayName: String, val curseforgeName: String, val modrinthName: String) {
|
import java.util.*
|
||||||
RELEASE("Release", "release", "release"),
|
import java.util.stream.Collectors
|
||||||
BETA("Beta", "beta", "beta"),
|
|
||||||
ALPHA("Alpha", "alpha", "alpha");
|
enum class VersionType(val displayName: String, val curseforgeName: String, val modrinthName: String, val semanticName: String, val shorthand: String): Comparable<VersionType> {
|
||||||
|
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]
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ import io.gitlab.jfronny.scripts.*
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import kotlin.jvm.optionals.getOrDefault
|
||||||
|
|
||||||
val isRelease = project.hasProperty("release")
|
val isRelease = project.hasProperty("release")
|
||||||
|
|
||||||
|
@ -14,24 +15,26 @@ if (File(projectDir, ".git").exists()) {
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
if (tags[0].fullMessage != null) changelog += "${tags[0].fullMessage}\n"
|
if (tags[0].fullMessage != null) changelog += "${tags[0].fullMessage}\n"
|
||||||
versionS = tags[0].name
|
versionS = tags[0].name
|
||||||
val vt = when(versionS[0]) {
|
val parsedVersion = SemanticVersion.parse(versionS)
|
||||||
'v' -> VersionType.RELEASE
|
versionType = parsedVersion.type
|
||||||
'b' -> VersionType.BETA
|
versionS = parsedVersion.unclassifiedToString()
|
||||||
'a' -> VersionType.ALPHA
|
lastRelease = versionS
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
versionType = vt ?: VersionType.RELEASE
|
|
||||||
if (vt != null) versionS = versionS.substring(1)
|
|
||||||
if (isRelease) {
|
if (isRelease) {
|
||||||
changelog += "Commits in ${versionType.displayName} $versionS:\n"
|
changelog += "Commits in ${versionType.displayName} $versionS:\n"
|
||||||
changelog += git.log(if (tags.size >= 2) tags[1].peeledId else null, tags[0].peeledId)
|
changelog += git.log(if (tags.size >= 2) tags[1].peeledId else null, tags[0].peeledId)
|
||||||
.reversed()
|
.reversed()
|
||||||
.joinToString("\n") { "- ${it.shortMessage}" }
|
.joinToString("\n") { "- ${it.shortMessage}" }
|
||||||
|
nextRelease = parsedVersion
|
||||||
} else {
|
} else {
|
||||||
changelog += "Commits after ${versionType.displayName} $versionS:\n"
|
changelog += "Commits after ${versionType.displayName} $versionS:\n"
|
||||||
changelog += git.log(tags[0].peeledId, git.repository.resolve("HEAD"))
|
val log = git.log(tags[0].peeledId, git.repository.resolve("HEAD")).reversed()
|
||||||
.reversed()
|
changelog += log.joinToString("\n") { "- ${it.shortMessage}" }
|
||||||
.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 {
|
} else {
|
||||||
changelog += "Commits after inception:\n"
|
changelog += "Commits after inception:\n"
|
||||||
|
@ -43,14 +46,30 @@ if (File(projectDir, ".git").exists()) {
|
||||||
} else changelog = "No changelog"
|
} else changelog = "No changelog"
|
||||||
|
|
||||||
if (!isRelease) {
|
if (!isRelease) {
|
||||||
versionS += "-SNAPSHOT"
|
versionS = "$nextRelease-SNAPSHOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
println(changelog)
|
println(changelog)
|
||||||
|
|
||||||
tasks.register("copyVersionNumber") {
|
tasks.register("copyVersionNumber") {
|
||||||
|
description = "Copy the current version number to the system clipboard"
|
||||||
doLast {
|
doLast {
|
||||||
Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(versionS), null)
|
Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(versionS), null)
|
||||||
println("Copied version number: $versionS")
|
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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue