Skip to content

Commit

Permalink
support "master" as main branch, add Branch stage type, add Flat mode…
Browse files Browse the repository at this point in the history
…, swap out underlying version library
  • Loading branch information
nefilim committed Jan 27, 2022
1 parent 2a20f36 commit 37de167
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 93 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The `Stage` property refers to the label (typically for pre-release builds) foll
* Alpha - `1.2.3-alpha.6`
* Beta - `1.2.3-beta.2`
* RC - `1.2.3-rc.3`
* Branch - `1.2.3-my_branch.1` this stage will use the branch name (everything after the last /) as the stage name, this can be useful on a high traffic repo to avoid version collisions in concurrent feature branches

### Main Branch
Expand All @@ -44,6 +45,8 @@ when the last version tag on main is `v1.2.3` the new version would be calculate
*Default Stage*: `Final`
*Default Scope*: `Minor`

`master` is also supported as the "main" branch in case it has not been renamed yet.

### Develop Branch

The `develop` branch should be rebased from main before releasing (eg locally or to a staging system).
Expand Down Expand Up @@ -78,6 +81,10 @@ Feature branches can be customized with a regex to match the branch name, eg:
*Hotfix Default Stage*: `Beta`
*Hotfix Default Scope*: `Patch`

#### Flat Mode

In case there is no `develop` branch the plugin will revert to "flat" mode which just bases all version decisions of `main` and assumes all branches were branched off `main`.

## Usage

```kotlin
Expand Down
26 changes: 22 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import java.time.ZoneId
import org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION as KOTLIN_VERSION

@Suppress("DSL_SCOPE_VIOLATION")
Expand Down Expand Up @@ -26,6 +25,26 @@ semver {
}
}

val invalidQualifiers = setOf("alpha", "beta", "rc", "nightly")
fun hasInvalidQualifier(candidate: ModuleComponentIdentifier): Boolean {
return invalidQualifiers.any { candidate.version.contains(it) }
}
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(libs.versions.kotlin.get())
}
}
componentSelection {
all {
if (!(candidate.group.startsWith("com.figure") || (candidate.group.startsWith("io.provenance"))) && hasInvalidQualifier(candidate))
reject("invalid qualifier versions for $candidate")
}
}
}
}

/*
* Project information
*/
Expand All @@ -46,16 +65,15 @@ repositories {
mavenCentral()
}


dependencies {
api(gradleApi())
api(gradleKotlinDsl())
api(kotlin("stdlib-jdk8"))
implementation(libs.arrow.core)
implementation(libs.eclipse.jgit.eclipseJgit)
api(libs.javiersc.semver.semverCore)
api(libs.swiftzer.semver)
testImplementation(gradleTestKit())
testImplementation(libs.bundles.kotlin.testing)
testImplementation(libs.bundles.kotest)
}

// Enforce Kotlin version coherence
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
kotlin.code.style=official
semver.currentBranch.scope=patch
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ kotlin = "1.6.10"

arrow = "1.0.1"
jgit = "6.0.0.202111291000-r"
kotest = "5.0.3"
semverCore = "0.1.0-beta.10"
kotest = "5.1.0"
kotestExtensionsArrow = "1.2.1"

gradle-benmanes-versions = "0.41.0"
gradle-changelog = "1.11.1"
Expand All @@ -14,14 +14,15 @@ github-release = "2.2.12"
gradle-semverPlugin = "0.0.28"

[libraries]
kotest-junit5-jvm = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" }
kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
javiersc-semver-semverCore = { module = "com.javiersc.semver:semver-core", version.ref = "semverCore" }
eclipse-jgit-eclipseJgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
kotest-junit5-jvm = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" }
kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" }
kotest-assertions-arrow = { module = "io.kotest.extensions:kotest-assertions-arrow", version.ref = "kotestExtensionsArrow" }
swiftzer-semver = { module = "net.swiftzer.semver:semver", version = "1.2.0" }

[bundles]
kotlin-testing = [ "kotest-junit5-jvm", "kotest-assertions-core-jvm" ]
kotest = [ "kotest-junit5-jvm", "kotest-assertions-core-jvm", "kotest-assertions-arrow" ]

[plugins]
changelog = { id = "org.hildan.github.changelog", version.ref = "gradle-changelog" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import io.github.nefilim.gradle.semver.config.SemVerPluginContext
import io.github.nefilim.gradle.semver.config.Stage
import io.github.nefilim.gradle.semver.domain.GitRef
import io.github.nefilim.gradle.semver.domain.SemVerError
import com.javiersc.semver.Version
import com.javiersc.semver.Version.Increase
import net.swiftzer.semver.SemVer

@Suppress("ComplexMethod")
internal fun SemVerPluginContext.calculatedVersionFlow(
main: GitRef.MainBranch,
develop: GitRef.DevelopBranch,
currentBranch: GitRef.Branch,
): Either<SemVerError, Version> {
): Either<SemVerError, SemVer> {
val tags = git.tagMap(config.tagPrefix)
verbose("calculating version for current branch $currentBranch, main: $main, develop: $develop")
return when (currentBranch) {
Expand All @@ -27,7 +26,7 @@ internal fun SemVerPluginContext.calculatedVersionFlow(
warn("unable to determine last version on main branch, using initialVersion [${config.initialVersion}]")
config.initialVersion.right()
},{
applyScopeToVersion(it, main.scope, main.stage)
applyScopeToVersion(currentBranch, it, main.scope, main.stage)
})
}
is GitRef.DevelopBranch -> {
Expand All @@ -39,7 +38,7 @@ internal fun SemVerPluginContext.calculatedVersionFlow(
it.getOrElse {
warn("unable to determine last version from main branch, using initialVersion [${config.initialVersion}]")
config.initialVersion
}.copy(stageNum = commitCount)
}.applyStageNumber(commitCount)
}.bind()
}
}
Expand All @@ -52,7 +51,7 @@ internal fun SemVerPluginContext.calculatedVersionFlow(
devVersion.fold({
SemVerError.MissingVersion("unable to find version tag on develop branch, feature branches must be branched from develop").left()
}, {
applyScopeToVersion(it, currentBranch.scope, currentBranch.stage).map { it.copy(stageNum = commitCount) }
applyScopeToVersion(currentBranch, it, currentBranch.scope, currentBranch.stage).map { it.applyStageNumber(commitCount) }
}).bind()
}
}
Expand All @@ -65,44 +64,63 @@ internal fun SemVerPluginContext.calculatedVersionFlow(
devVersion.fold({
SemVerError.MissingVersion("unable to find version tag on main branch, hotfix branches must be branched from main").left()
}, {
applyScopeToVersion(it, currentBranch.scope, currentBranch.stage).map { it.copy(stageNum = commitCount) }
applyScopeToVersion(currentBranch, it, currentBranch.scope, currentBranch.stage).map { it.applyStageNumber(commitCount) }
}).bind()
}
}
}
}

internal fun applyScopeToVersion(version: Version, scope: Scope, stage: Stage? = null): Either<SemVerError, Version> {
return when (scope) {
Scope.Major -> {
when (stage) {
Stage.Snapshot -> version.nextSnapshotMajor().right()
else -> version.inc(Increase.Major, stage.toStageName()).right()
}
}
Scope.Minor -> {
when (stage) {
Stage.Snapshot -> version.nextSnapshotMinor().right()
else -> version.inc(Increase.Minor, stage.toStageName()).right()
}
internal fun SemVerPluginContext.calculatedVersionFlat(
main: GitRef.MainBranch,
currentBranch: GitRef.Branch,
): Either<SemVerError, SemVer> {
val tags = git.tagMap(config.tagPrefix)
verbose("calculating flat version for current branch $currentBranch, main: $main")
return when (currentBranch) {
is GitRef.MainBranch -> {
main.version.fold({
warn("unable to determine last version on main branch, using initialVersion [${config.initialVersion}]")
config.initialVersion.right()
},{
applyScopeToVersion(currentBranch, it, main.scope, main.stage)
})
}
Scope.Patch ->
when (stage) {
Stage.Snapshot -> version.nextSnapshotPatch().right()
else -> version.inc(Increase.Patch, stage.toStageName()).right()
else -> {
// recalculate version automatically based on releases on main
either.eager {
val branchPoint = git.headRevInBranch(main).bind()
val commitCount = commitsSinceBranchPoint(branchPoint, currentBranch, tags).getOrElse { 0 }
git.calculateBaseBranchVersion(main, currentBranch, tags).map {
it.getOrElse {
warn("unable to determine last version from main branch, using initialVersion [${config.initialVersion}]")
config.initialVersion
}.applyStageNumber(commitCount)
}.bind()
}
Scope.Auto -> {
SemVerError.UnsupportedScope(scope).left()
}
}
}

internal fun applyScopeToVersion(currentBranch: GitRef.Branch, version: SemVer, scope: Scope, stage: Stage): Either<SemVerError, SemVer> {
return when (scope) {
Scope.Major -> version.nextMajor().copy(preRelease = stage.toStageName(currentBranch)).right()
Scope.Minor -> version.nextMinor().copy(preRelease = stage.toStageName(currentBranch)).right()
Scope.Patch -> version.nextPatch().copy(preRelease = stage.toStageName(currentBranch)).right()
}
}

internal fun SemVer.applyStageNumber(stageNumber: Int): SemVer {
return this.copy(preRelease = if (this.preRelease.isNullOrBlank()) "1" else "${this.preRelease}.$stageNumber")
}

// TODO create an ADT for Stage so we can derive string name here
internal fun Stage?.toStageName(): String {
internal fun Stage?.toStageName(currentBranch: GitRef.Branch): String {
return when (this) {
null -> ""
Stage.Final -> ""
Stage.Snapshot -> "SNAPSHOT"
Stage.Branch -> currentBranch.name.substringAfterLast('/')
else -> this.name.lowercase()
}
}
49 changes: 32 additions & 17 deletions src/main/kotlin/io/github/nefilim/gradle/semver/Git.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import arrow.core.left
import arrow.core.right
import arrow.core.some
import arrow.core.toOption
import com.javiersc.semver.Version
import io.github.nefilim.gradle.semver.config.PluginConfig
import io.github.nefilim.gradle.semver.config.SemVerPluginContext
import io.github.nefilim.gradle.semver.domain.GitRef
import io.github.nefilim.gradle.semver.domain.SemVerError
import net.swiftzer.semver.SemVer
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Ref
Expand All @@ -24,13 +24,24 @@ import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.gradle.api.Project

internal fun String?.semverTag(prefix: String): Option<Version> {
internal fun Git.currentBranchRef(): Option<String> {
return if (githubActionsBuild() && pullRequestEvent()) {
pullRequestHeadRef()
} else
repository.fullBranch.some()
}

internal fun String?.semverTag(prefix: String): Option<SemVer> {
return this?.substringAfterLast("/$prefix")?.let {
Version.safe(it).fold({ it.some() }, { None })
if (it.isNotBlank() && it.count { it == '.' } == 2)
Either.catch { SemVer.parse(it) }.fold({ None }, { it.some() })
else
None
} ?: None
}

internal fun Ref?.semverTag(prefix: String): Option<Version> {
internal fun Ref?.semverTag(prefix: String): Option<SemVer> {
// println("checking semver tag for ${this?.name}")
return this?.name?.semverTag(prefix) ?: None
}

Expand All @@ -50,25 +61,27 @@ private fun Git.buildRef(refName: String): Either<SemVerError, Ref> {
.flatMap { it.toEither { SemVerError.MissingRef("could not find a git ref for [$refName]") } }
}

internal fun Git.tagMap(prefix: String): Map<ObjectId, Version> {
internal fun Git.tagMap(prefix: String): Map<ObjectId, SemVer> {
val versionTags = tagList().call().toList().map { ref ->
// have to unpeel annotated tags
ref.semverTag(prefix).map { (repository.refDatabase.peel(ref).peeledObjectId ?: ref.objectId) to it }
}.flattenOption()
return versionTags.toMap()
}

internal fun Git.currentVersion(config: PluginConfig, branchRefName: String): Option<Version> {
internal fun Git.currentVersion(config: PluginConfig, branchRefName: String): Option<SemVer> {
val tags = tagMap(config.tagPrefix)
val tagsIDs = tagMap(config.tagPrefix).keys
val tagsIDs = tags.keys
return RevWalk(repository).use { walk ->
val head = walk.parseCommit(GitRef.Branch.headCommitID(repository, branchRefName))

walk.markStart(head)
(walk.firstOrNull() {
tagsIDs.contains(it.toObjectId()) &&
tags.containsKey(it.toObjectId())
}?.let { tags[it.toObjectId()].toOption() } ?: None).also {
}?.let {
tags[it.toObjectId()].toOption()
} ?: None).also {
walk.dispose()
}
}
Expand All @@ -77,17 +90,19 @@ internal fun Git.currentVersion(config: PluginConfig, branchRefName: String): Op
internal fun SemVerPluginContext.buildBranch(branchRefName: String, config: PluginConfig): Either<SemVerError, GitRef.Branch> {
return either.eager {
val shortName = git.buildRef(branchRefName).flatMap { it.shortName() }.bind()
verbose("building branch for $branchRefName")
with (shortName) {
when {
equals("main") -> {
equals(GitRef.MainBranch.Name, ignoreCase = true) || equals(GitRef.MainBranch.AlternativeName, ignoreCase = true) -> {
GitRef.MainBranch(
GitRef.MainBranch.determineName(branchRefName),
branchRefName,
git.currentVersion(config, branchRefName),
config.currentBranchScope.getOrElse { GitRef.MainBranch.DefaultScope },
config.currentBranchStage.getOrElse { GitRef.MainBranch.DefaultStage }
).right()
}
equals("develop") -> {
equals(GitRef.DevelopBranch.Name, ignoreCase = true) -> {
GitRef.DevelopBranch(
branchRefName,
config.currentBranchScope.getOrElse { GitRef.DevelopBranch.DefaultScope },
Expand Down Expand Up @@ -119,7 +134,7 @@ internal fun SemVerPluginContext.buildBranch(branchRefName: String, config: Plug
internal fun SemVerPluginContext.commitsSinceBranchPoint(
branchPoint: RevCommit,
branch: GitRef.Branch,
tags: Map<ObjectId, Version>,
tags: Map<ObjectId, SemVer>,
): Either<SemVerError, Int> {
val commits = git.log().call().toList() // can this blow up for large repos?
val newCommits = commits.takeWhile {
Expand All @@ -143,14 +158,14 @@ internal fun SemVerPluginContext.commitsSinceBranchPoint(
internal fun Git.calculateBaseBranchVersion(
branchTarget: GitRef.Branch,
branch: GitRef.Branch,
tags: Map<ObjectId, Version>,
): Either<SemVerError, Option<Version>> {
tags: Map<ObjectId, SemVer>,
): Either<SemVerError, Option<SemVer>> {
return either.eager {
val head = headRevInBranch(branch).bind()
findYoungestTagOnBranchOlderThanTarget(branchTarget, head, tags).fold({
None.right()
}, {
applyScopeToVersion(it, branch.scope, branch.stage).map { it.some() }
applyScopeToVersion(branch, it, branch.scope, branch.stage).map { it.some() }
}).bind()
}
}
Expand All @@ -170,8 +185,8 @@ internal fun Git.headRevInBranch(branch: GitRef.Branch): Either<SemVerError, Rev
internal fun Git.findYoungestTagOnBranchOlderThanTarget(
branch: GitRef.Branch,
target: RevCommit,
tags: Map<ObjectId, Version>
): Option<Version> {
tags: Map<ObjectId, SemVer>
): Option<SemVer> {
return log().add(repository.exactRef(branch.refName).objectId).call()
.firstOrNull { it.commitTime <= target.commitTime && tags.containsKey(it.toObjectId()) }
.toOption()
Expand All @@ -180,7 +195,7 @@ internal fun Git.findYoungestTagOnBranchOlderThanTarget(

internal fun Git.findYoungestTagCommitOnBranch(
branch: GitRef.Branch,
tags: Map<ObjectId, Version>
tags: Map<ObjectId, SemVer>
): Option<RevCommit> {
return log().add(repository.exactRef(branch.refName).objectId).call()
.firstOrNull { tags.containsKey(it.toObjectId()) }
Expand Down
Loading

0 comments on commit 37de167

Please sign in to comment.