All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.serpro69.semverkt.release.SemverRelease.kt Maven / Gradle / Ivy

There is a newer version: 0.13.0
Show newest version
package io.github.serpro69.semverkt.release

import io.github.serpro69.semverkt.release.configuration.Configuration
import io.github.serpro69.semverkt.release.configuration.GitMessageConfig
import io.github.serpro69.semverkt.release.configuration.GitTagConfig
import io.github.serpro69.semverkt.release.configuration.VersionConfig
import io.github.serpro69.semverkt.release.repo.Commit
import io.github.serpro69.semverkt.release.repo.GitRepository
import io.github.serpro69.semverkt.release.repo.Message
import io.github.serpro69.semverkt.release.repo.Repository
import io.github.serpro69.semverkt.release.repo.semver
import io.github.serpro69.semverkt.spec.PreRelease
import io.github.serpro69.semverkt.spec.Semver

/**
 * Provides functions for semantic releases of a project [Repository].
 *
 * @property currentVersion the current (last) version in a given repository
 */
class SemverRelease : AutoCloseable {
    private val repo: Repository
    private val config: Configuration

    val currentVersion: () -> Semver?

    /**
     * @constructor Creates an instance of [SemverRelease] with given [configuration] parameters.
     */
    constructor(configuration: Configuration) {
        repo = GitRepository(configuration)
        config = configuration
        currentVersion = { repo.latestVersionTag()?.let { semver(configuration.git.tag)(it) } }
    }

    /**
     * @constructor Creates an instance of [SemverRelease] for a given [repository].
     */
    constructor(repository: Repository) {
        repo = repository
        config = repo.config
        currentVersion = { repo.latestVersionTag()?.let { semver(config.git.tag)(it) } }
    }

    override fun close() {
        repo.close()
    }

    /**
     * Returns the [version] IF it's not equal to [currentVersion] AND not already present in the [repo],
     * ELSE returns `null`.
     */
    fun release(version: Semver): Semver? {
        val moreThanCurrent = currentVersion()?.let { version > it } ?: true
        val exists by lazy { repo.tags().map { semver(config.git.tag)(it) }.any { it == version } }
        return if (moreThanCurrent && !exists) version else null
    }

    /**
     * Returns the next release version after the [currentVersion] based on the [increment].
     *
     * See also [SemverRelease.increment] for the rules that are used when releasing the versions.
     */
    fun release(increment: Increment): Semver = increment(increment, true)

    /**
     * Returns the next snapshot version after the [currentVersion] based on the [increment].
     *
     * See also [SemverRelease.increment] for the rules that are used when creating version snapshots.
     */
    fun snapshot(increment: Increment): Semver = increment(increment, false)

    /**
     * Increment the [currentVersion] to the next [Semver] using the [increment].
     *
     * The following rules apply when incrementing the [currentVersion]:
     *
     * 1. **IF the [currentVersion] is `null`.**
     * THEN initial version is returned as configured by [VersionConfig.initialVersion].
     *
     * 2. **The [increment] is a [Increment.PRE_RELEASE].**
     * IF [currentVersion] is a pre-release version,
     * THEN the [Semver.preRelease] component is incremented,
     * ELSE the [currentVersion] is returned.
     *
     * 3. **The [increment] is [Increment.DEFAULT].**
     * IF [currentVersion] is a pre-release version,
     * THEN the [Semver.preRelease] component is incremented,
     * ELSE the [currentVersion] is incremented using [VersionConfig.defaultIncrement].
     *
     * 4. **If the [increment] is [Increment.NONE],**
     * THEN the [currentVersion] is returned.
     *
     * @param increment the version component to increment
     * @param release   if `false` appends [VersionConfig.snapshotSuffix] to the final version
     */
    fun increment(
        increment: Increment,
        release: Boolean = (increment != Increment.DEFAULT),
    ): Semver {
        val nextVersion = currentVersion()?.let {
            when (increment) {
                Increment.MAJOR -> it.incrementMajor()
                Increment.MINOR -> it.incrementMinor()
                Increment.PATCH -> it.incrementPatch()
                Increment.PRE_RELEASE -> {
                    with(it.incrementPreRelease()) {
                        preRelease?.let { _ -> if (release) this else withSnapshot() } ?: this
                    }
                }
                Increment.DEFAULT -> {
                    it.preRelease?.let { _ -> increment(Increment.PRE_RELEASE, release) }
                        ?: increment(config.version.defaultIncrement, release)
                }
                Increment.NONE -> currentVersion()
            }
        } ?: config.version.initialVersion
        return if (release || increment == Increment.PRE_RELEASE) nextVersion else nextVersion.withSnapshot()
    }

    /**
     * Creates a pre-release version with a given [increment], and returns as [Semver] instance.
     *
     * The following rules apply when creating a new pre-release version:
     *
     * 1. **WHEN [currentVersion] is a pre-release version.**
     * IF the [currentVersion] is a snapshot version (ends with the [VersionConfig.snapshotSuffix]),
     * OR the [increment] is [Increment.PRE_RELEASE] or [Increment.NONE]
     * THEN the [currentVersion] is returned,
     * ELSE increases the [currentVersion] to the next [increment],
     * and appends the [PreRelease] to it with the [VersionConfig.initialPreRelease] number.
     *
     * 2. **WHEN [currentVersion] is `null`,**
     * THEN a new pre-release version consisting of [VersionConfig.initialVersion] and [VersionConfig.initialPreRelease] is returned,
     *
     * 3. **WHEN [currentVersion] is a release version.**
     * IF [increment] is one of [Increment.MAJOR], [Increment.MINOR], [Increment.PATCH] or [Increment.DEFAULT],
     * THEN increases the [currentVersion] to the next [increment]
     *   and appends the [PreRelease] to it with the [VersionConfig.initialPreRelease] number,
     * ELSE the [currentVersion] is returned.
     */
    fun createPreRelease(increment: Increment): Semver {
        val preRelease = PreRelease("${config.version.preReleaseId}.${config.version.initialPreRelease}")
        return with(currentVersion()) {
            this?.preRelease?.let {
                if (this.isSnapshot() || increment in listOf(Increment.PRE_RELEASE, Increment.NONE)) this else {
                    val nextVer = if (increment == Increment.DEFAULT) {
                        release(config.version.defaultIncrement)
                    } else release(increment)
                    nextVer.copy(preRelease = preRelease)
                }
            } ?: run {
                if (increment in listOf(Increment.PRE_RELEASE, Increment.NONE)) {
                    this ?: release(increment).copy(preRelease = preRelease)
                } else {
                    release(increment).copy(preRelease = preRelease)
                }
            }
        }
    }

    private fun Semver.isSnapshot(): Boolean = toString().endsWith("-${config.version.snapshotSuffix}")

    /**
     * Promotes a pre-release version to a release version.
     *
     * IF [currentVersion] is a pre-release version,
     * THEN a copy of the [currentVersion] is returned as a normal version (with the pre-release component stripped),
     * ELSE the [currentVersion] version is returned.
     */
    fun promoteToRelease(): Semver? {
        return with(currentVersion()) {
            this?.preRelease?.let { copy(preRelease = null, buildMetadata = null) } ?: this
        }
    }

    /**
     * Returns the next [Increment] based on the [Repository.latestVersionTag].
     *
     * IF any of the commits contain a keyword for incrementing a specific version, as configured by [GitMessageConfig],
     * THEN that increment will be returned based on the precedence rules,
     * ELSE [Increment.NONE] is returned.
     *
     * IF the repository `HEAD` points to the [Repository.latestVersionTag],
     * OR the repository `HEAD` points to any other existing release tag, as configured by [GitTagConfig.prefix],
     * THEN [Increment.NONE] is returned.
     *
     * Precedence rules for commit message keywords, from highest to lowest:
     * - [GitMessageConfig.major]
     * - [GitMessageConfig.minor]
     * - [GitMessageConfig.patch]
     * - [GitMessageConfig.preRelease]
     */
    fun nextIncrement(): Increment {
        val head = repo.head()
        val latestTag = repo.latestVersionTag()
        val tagObjectId = latestTag?.peeledObjectId ?: latestTag?.objectId
        val isHeadOnTag by lazy { repo.tags().any { head == it.peeledObjectId || head == it.objectId } }
        return if (head == tagObjectId || isHeadOnTag) Increment.NONE else repo.log(latestTag).nextIncrement()
    }

    private fun List.nextIncrement(): Increment {
        var inc = Increment.DEFAULT
        forEach { c ->
            if (c.message.full().contains(config.git.message.major)) return Increment.MAJOR
            if (inc > Increment.MINOR && c.message.full().contains(config.git.message.minor)) {
                inc = Increment.MINOR
                return@forEach
            }
            if (inc > Increment.PATCH && c.message.full().contains(config.git.message.patch)) {
                inc = Increment.PATCH
                return@forEach
            }
            if (inc > Increment.PRE_RELEASE && c.message.full().contains(config.git.message.preRelease)) {
                inc = Increment.PRE_RELEASE
            }
        }
        return inc
    }

    private fun Semver.withSnapshot(): Semver {
        val pr = preRelease?.toString()?.let {
            when {
                it.endsWith(config.version.snapshotSuffix) -> it
                else -> "${it}-${config.version.snapshotSuffix}"
            }
        } ?: config.version.snapshotSuffix
        return copy(preRelease = PreRelease(pr), buildMetadata = null)
    }

    private fun Message.full(): String {
        return with(StringBuilder()) {
            appendLine(title)
            appendLine()
            appendLine(description.joinToString("\n"))
            toString()
        }
    }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy