diff --git a/librarian-cli/src/main/kotlin/com/gradleup/librarian/cli/command/TagAndBump.kt b/librarian-cli/src/main/kotlin/com/gradleup/librarian/cli/command/TagAndBump.kt index 1bd0e21..7491c19 100644 --- a/librarian-cli/src/main/kotlin/com/gradleup/librarian/cli/command/TagAndBump.kt +++ b/librarian-cli/src/main/kotlin/com/gradleup/librarian/cli/command/TagAndBump.kt @@ -2,12 +2,29 @@ package com.gradleup.librarian.cli.command import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.optional +import com.github.kinquirer.KInquirer +import com.github.kinquirer.components.promptConfirm +import com.gradleup.librarian.core.tooling.BumpMajor +import com.gradleup.librarian.core.tooling.DowngradeVersion +import com.gradleup.librarian.core.tooling.UseDefaultVersion import com.gradleup.librarian.core.tooling.tagAndBump +import kotlin.system.exitProcess internal class TagAndBump: CliktCommand() { - val versionToRelease by argument() + val versionToRelease by argument().optional() override fun run() { - tagAndBump(versionToRelease) + tagAndBump(versionToRelease) { + val ret = when(it) { + is BumpMajor -> KInquirer.promptConfirm("Bump major version to '${it.tagVersion}' and bump? (current version is ${it.currentVersion})", true) + is DowngradeVersion -> KInquirer.promptConfirm("Downgrade version to '${it.tagVersion}' and bump? (current version is ${it.currentVersion})", true) + is UseDefaultVersion -> KInquirer.promptConfirm("Tag version '${it.expectedVersion}' and bump?", true) + } + + if (!ret) { + exitProcess(1) + } + } } } \ No newline at end of file diff --git a/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/semver.kt b/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/semver.kt index f3f6e57..c9ba6f6 100644 --- a/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/semver.kt +++ b/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/semver.kt @@ -42,9 +42,94 @@ internal fun getNextSnapshot(version: String): String { return getNextPatch(version) + "-SNAPSHOT" } -/** - * Mostly used as a sanity check - */ -internal fun String.isValidVersion(): Boolean { - return Regex("[0-9]+\\.[0-9]+\\.[0-9]+.*").matches(this) -} \ No newline at end of file +internal class PreRelease( + val name: String, + val version: Int, +) { + +} + +internal fun PreRelease?.compareTo(other: PreRelease?): Int { + return if (this == null && other == null) { + 0 + } else if (this == null) { + 1 + } else if (other == null) { + -1 + } else { + // XXX: should we handle non-lexicographic order here? + val ret = name.compareTo(other.name) + if (ret != 0) { + return ret + } + + return version.compareTo(other.version) + } +} + +internal class Version( + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: PreRelease?, + val isSnapshot: Boolean, +) { + operator fun compareTo(currentVersionParsed: Version): Int { + var ret = major.compareTo(currentVersionParsed.major) + if (ret != 0) return ret + + ret = minor.compareTo(currentVersionParsed.minor) + if (ret != 0) return ret + + ret = patch.compareTo(currentVersionParsed.patch) + if (ret != 0) return ret + + ret = preRelease.compareTo(currentVersionParsed.preRelease) + if (ret != 0) return ret + + ret = major.compareTo(currentVersionParsed.major) + if (ret != 0) return ret + + return 0 + } +} + +internal fun String.toVersionOrNull(): Version? { + val regex1 = Regex("([0-9]+)\\.([0-9]+)\\.([0-9]+)(.*)") + + val result1 = regex1.matchEntire(this) ?: return null + + val major = result1.groupValues[1].toIntOrNull() ?: return null + val minor = result1.groupValues[2].toIntOrNull() ?: return null + val patch = result1.groupValues[3].toIntOrNull() ?: return null + + var rem = result1.groupValues[4] + if (rem.isEmpty()) { + return Version(major, minor, patch, null, false) + } + + var preRelease: PreRelease? = null + + val snapshot = rem.endsWith("-SNAPSHOT") + if (snapshot) { + rem = rem.removeSuffix("-SNAPSHOT") + } + if (!rem.isEmpty()) { + if (!rem.startsWith("-")) { + return null + } + rem = rem.substring(1) + val regex2 = Regex("([a-zA-Z]+)\\.([0-9]+)") + val result2 = regex2.matchEntire(rem) ?: return null + + val v = result2.groupValues[2].toIntOrNull() ?: return null + preRelease = PreRelease(result2.groupValues[1], v) + } + return Version( + major, + minor, + patch, + preRelease, + snapshot + ) +} diff --git a/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/tag-and-bump.kt b/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/tag-and-bump.kt index be07ea8..1c5ee9b 100644 --- a/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/tag-and-bump.kt +++ b/librarian-core/src/main/kotlin/com/gradleup/librarian/core/tooling/tag-and-bump.kt @@ -3,14 +3,54 @@ package com.gradleup.librarian.core.tooling import java.io.File fun tagAndBump(versionToRelease: String) { + return tagAndBump(versionToRelease) { + error("versionToRelease must not be null") + } +} + +sealed interface Confirmation +class UseDefaultVersion( + val expectedVersion: String +): Confirmation + +class DowngradeVersion( + val tagVersion: String, + val currentVersion: String +): Confirmation + +class BumpMajor( + val tagVersion: String, + val currentVersion: String +): Confirmation + +fun tagAndBump(versionToRelease: String?, confirm: (Confirmation) -> Unit) { checkCwd() - check(versionToRelease.isValidVersion()) { - "Version must start with 'major.minor.patch' (found '$versionToRelease')" + var tagVersion = versionToRelease + val currentVersion = getCurrentVersion() + val currentVersionParsed = currentVersion.toVersionOrNull() + check(currentVersionParsed != null) { + "Cannot parse current version: '${currentVersion}'" } + check(currentVersionParsed.isSnapshot) { + "Version '${currentVersion} is not a -SNAPSHOT, check your working directory" + } + + val expectedVersion = currentVersion.removeSuffix("-SNAPSHOT") - check(getCurrentVersion().endsWith("-SNAPSHOT")) { - "Version '${getCurrentVersion()} is not a -SNAPSHOT, check your working directory" + if (tagVersion == null) { + confirm(UseDefaultVersion(expectedVersion)) + tagVersion = expectedVersion + } + + val tagVersionParsed = tagVersion.toVersionOrNull() + check(tagVersionParsed != null) { + "Version must start with 'major.minor.patch' (found '$tagVersion')" + } + if (tagVersionParsed < currentVersionParsed) { + confirm(DowngradeVersion(tagVersion, currentVersion)) + } else if (tagVersionParsed.major > currentVersionParsed.major) { + confirm(BumpMajor(tagVersion, currentVersion)) } check(runCommand("git", "status", "--porcelain").isEmpty()) { @@ -22,14 +62,14 @@ fun tagAndBump(versionToRelease: String) { "You must be on the main branch or a release branch to make a release" } - val markdown = processChangelog(versionToRelease) + val markdown = processChangelog(tagVersion) // 'De-snapshot' the version, open a PR, and merge it - val releaseBranchName = "tag-$versionToRelease" + val releaseBranchName = "tag-$tagVersion" runCommand("git", "checkout", "-b", releaseBranchName) - setCurrentVersion(versionToRelease) - setVersionInDocs(versionToRelease) - runCommand("git", "commit", "-a", "-m", "release $versionToRelease") + setCurrentVersion(tagVersion) + setVersionInDocs(tagVersion) + runCommand("git", "commit", "-a", "-m", "release $tagVersion") runCommand("git", "push", "origin", releaseBranchName) runCommand("gh", "pr", "create", "--base", startBranch, "--fill") @@ -39,17 +79,17 @@ fun tagAndBump(versionToRelease: String) { // Tag the release, and push the tag runCommand("git", "checkout", startBranch) runCommand("git", "pull", "origin", startBranch) - val tagName = "v$versionToRelease" + val tagName = "v$tagVersion" runCommand("git", "tag", tagName, "-m", markdown) runCommand("git", "push", "origin", tagName) println("Tag pushed.") // Bump the version to the next snapshot - val bumpVersionBranchName = "bump-$versionToRelease" + val bumpVersionBranchName = "bump-$tagVersion" runCommand("git", "checkout", "-b", bumpVersionBranchName) - val nextSnapshot = getNextSnapshot(versionToRelease) + val nextSnapshot = getNextSnapshot(tagVersion) setCurrentVersion(nextSnapshot) runCommand("git", "commit", "-a", "-m", "version is now $nextSnapshot") runCommand("git", "push", "origin", bumpVersionBranchName) diff --git a/librarian-core/src/main/resources/gitignore b/librarian-core/src/main/resources/gitignore index 3c00b8d..4e67eeb 100644 --- a/librarian-core/src/main/resources/gitignore +++ b/librarian-core/src/main/resources/gitignore @@ -1,18 +1,19 @@ +.kotlin .fleet .gradle +# Build outputs +build + # Idea **/.idea/* !**/.idea/codeStyles !**/.idea/icon.png !**/.idea/runConfigurations !**/.idea/scopes +!**/.idea/dictionaries *.iml -# Generated files -build -.kotlin - # Place where the Android SDK path is set local.properties diff --git a/librarian-core/src/test/kotlin/SemVerTest.kt b/librarian-core/src/test/kotlin/SemVerTest.kt new file mode 100644 index 0000000..969bdd0 --- /dev/null +++ b/librarian-core/src/test/kotlin/SemVerTest.kt @@ -0,0 +1,44 @@ +import com.gradleup.librarian.core.tooling.toVersionOrNull +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class SemVerTest { + @Test + fun test() { + "1.2.3-alpha.0".toVersionOrNull().apply { + assertNotNull(this) + assertEquals(1, major) + assertEquals(2, minor) + assertEquals(3, patch) + assertEquals(false, isSnapshot) + assertEquals("alpha", preRelease?.name) + assertEquals(0, preRelease?.version) + } + "1.2.3".toVersionOrNull().apply { + assertNotNull(this) + assertEquals(1, major) + assertEquals(2, minor) + assertEquals(3, patch) + assertEquals(false, isSnapshot) + assertEquals(null, preRelease) + } + "1.2.3-SNAPSHOT".toVersionOrNull().apply { + assertNotNull(this) + assertEquals(1, major) + assertEquals(2, minor) + assertEquals(3, patch) + assertEquals(true, isSnapshot) + assertEquals(null, preRelease) + } + "1.2.3-alpha.0-SNAPSHOT".toVersionOrNull().apply { + assertNotNull(this) + assertEquals(1, major) + assertEquals(2, minor) + assertEquals(3, patch) + assertEquals(true, isSnapshot) + assertEquals("alpha", preRelease?.name) + assertEquals(0, preRelease?.version) + } + } +} \ No newline at end of file diff --git a/librarian-gradle-plugin/src/main/kotlin/com/gradleup/librarian/gradle/publishing.kt b/librarian-gradle-plugin/src/main/kotlin/com/gradleup/librarian/gradle/publishing.kt index ba0e479..2453ada 100644 --- a/librarian-gradle-plugin/src/main/kotlin/com/gradleup/librarian/gradle/publishing.kt +++ b/librarian-gradle-plugin/src/main/kotlin/com/gradleup/librarian/gradle/publishing.kt @@ -13,6 +13,7 @@ import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.tasks.Jar +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import java.util.Properties internal const val librarianPublication = "librarianPublication" @@ -154,8 +155,9 @@ fun Project.createMissingPublications( when { project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") -> { /** - * Kotlin MPP creates publications. - * It only misses the javadoc + * Kotlin MPP creates publications but doesn't add javadoc. + * Note: for Android, the caller needs to opt-in puoblication + * See https://kotlinlang.org/docs/multiplatform-publish-lib.html#publish-an-android-library */ publications.withType(MavenPublication::class.java).configureEach { it.artifact(emptyJavadoc)