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

main.name.remal.gradle_plugins.plugins.vcs.AutoVcsVersionPlugin.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.plugins.vcs

import name.remal.KotlinAllOpen
import name.remal.asObservable
import name.remal.concurrentMapOf
import name.remal.default
import name.remal.gradle_plugins.dsl.ApplyPluginClasses
import name.remal.gradle_plugins.dsl.BaseReflectiveProjectPlugin
import name.remal.gradle_plugins.dsl.CreateExtensionsPluginAction
import name.remal.gradle_plugins.dsl.Extension
import name.remal.gradle_plugins.dsl.Plugin
import name.remal.gradle_plugins.dsl.PluginAction
import name.remal.gradle_plugins.dsl.WithPluginClasses
import name.remal.gradle_plugins.dsl.extensions.afterEvaluateOrNow
import name.remal.gradle_plugins.dsl.extensions.atTheEndOfAfterEvaluationOrNow
import name.remal.gradle_plugins.dsl.extensions.createWithAutoName
import name.remal.gradle_plugins.dsl.extensions.get
import name.remal.gradle_plugins.dsl.extensions.isPluginAppliedAndNotDisabled
import name.remal.gradle_plugins.dsl.extensions.setAllprojectsVersion
import name.remal.gradle_plugins.dsl.extensions.shouldRunAfter
import name.remal.gradle_plugins.dsl.plugins.DisplayProjectVersionPlugin
import name.remal.gradle_plugins.dsl.utils.getGradleLogger
import name.remal.gradle_plugins.plugins.ci.CommonCIPlugin
import name.remal.max
import name.remal.version.Version
import name.remal.version.VersionParsingException
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.tasks.TaskContainer
import java.io.File
import java.time.LocalDateTime
import java.time.ZoneOffset.UTC
import kotlin.text.RegexOption.IGNORE_CASE

@Plugin(
    id = "name.remal.vcs-auto-version",
    description = "Plugin that sets project version based on VCS history",
    tags = ["vcs"]
)
@ApplyPluginClasses(VcsOperationsPlugin::class, DisplayProjectVersionPlugin::class)
class AutoVcsVersionPlugin : BaseReflectiveProjectPlugin() {

    @CreateExtensionsPluginAction
    fun ExtensionContainer.`Create 'autoVcsVersion' extension`(project: Project) {
        create("autoVcsVersion", AutoVcsVersionExtension::class.java, project)
    }

    @PluginAction(order = -10000)
    fun TaskContainer.`Create 'writeAutoVcsVersionInFile' task`() {
        create("writeAutoVcsVersionInFile", WriteAutoVcsVersionInFileTask::class.java)
    }

    @PluginAction(isHidden = false, order = -100)
    fun Project.calculateAutoVcsVersion() {
        calculateAutoVcsVersion(false)
    }

    @PluginAction
    fun TaskContainer.`Create 'createAutoVcsVersionTag' task`() {
        create("createAutoVcsVersionTag", CreateProjectVersionTag::class.java) { task ->
            task.project.afterEvaluateOrNow(Int.MAX_VALUE) { project ->
                val autoVcsVersion = project[AutoVcsVersionExtension::class.java]
                task.tagNamePrefix = autoVcsVersion.versionTagPrefixes.firstOrNull().default()
            }

            task.shouldRunAfter {
                task.project.rootProject.allprojects.asSequence()
                    .map(Project::getTasks)
                    .filterIsInstance(BaseCreateTagTask::class.java)
                    .filter { it != task }
                    .toList()
            }
        }
    }

    @PluginAction(order = Int.MAX_VALUE)
    @WithPluginClasses(CommonCIPlugin::class)
    fun Project.`Log current auto VCS version info`() {
        atTheEndOfAfterEvaluationOrNow(Int.MAX_VALUE) { _ ->
            val autoVcsVersion = this[AutoVcsVersionExtension::class.java]
            val vcsOperations = this[VcsOperationsExtension::class.java]
            autoVcsVersion.currentMutable?.let {

                logger.lifecycle("VCS: current commit: {}", vcsOperations.getCurrentCommit())
                logger.lifecycle("VCS: current branch: {}", vcsOperations.currentBranch)
                logger.lifecycle("VCS: the latest base VCS version: {}", it.latestBaseVersion)
                logger.lifecycle("VCS: the latest VCS version: {}", it.latestVersion)
                if (it.useLatestVersion) {
                    logger.lifecycle("VCS: the latest VCS version should be used as a current version")
                } else if (autoVcsVersion.incrementVersionByCommitsCount && it.commitsAfterLatestVersion >= 1) {
                    logger.lifecycle("VCS: commits after the latest VCS version: {}", it.commitsAfterLatestVersion)
                }
                if (it.increment >= 1) {
                    logger.lifecycle("VCS: increment VCS version by: {}", it.increment)
                }
                logger.lifecycle("VCS: calculated VCS version: {}", it.version)

            }
        }
    }

}


@Extension
class AutoVcsVersionExtension(private val project: Project) {

    private var isCalculationEnabled: Boolean = true

    private inline fun changeSettings(action: () -> Unit) {
        isCalculationEnabled = false
        try {
            action()
            project.calculateAutoVcsVersion()
        } finally {
            isCalculationEnabled = true
        }
    }

    private fun Project.calculateAutoVcsVersion() {
        if (isCalculationEnabled) {
            calculateAutoVcsVersion(true)
        }
    }


    val versionTagPrefixes: MutableList = mutableListOf().asObservable().apply {
        addAll(listOf("ver-", "version-"))
        registerElementAddedHandler { project.calculateAutoVcsVersion() }
    }

    fun versionTagPrefix(versionTagPrefix: String) = changeSettings {
        versionTagPrefixes.add(versionTagPrefix)
    }

    fun versionTagPrefixes(versionTagPrefixes: Iterable) = changeSettings {
        this.versionTagPrefixes.addAll(versionTagPrefixes)
    }

    fun versionTagPrefixes(vararg versionTagPrefixes: String) = changeSettings {
        this.versionTagPrefixes.addAll(versionTagPrefixes)
    }

    fun setVersionTagPrefixes(versionTagPrefixes: Collection) = changeSettings {
        if (versionTagPrefixes.isEmpty()) throw IllegalArgumentException("versionTagPrefixes can't be empty")
        this.versionTagPrefixes.apply {
            clear()
            addAll(versionTagPrefixes)
        }
    }

    fun setVersionTagPrefixes(vararg versionTagPrefixes: String) = setVersionTagPrefixes(versionTagPrefixes.toList())


    val baseVersionTagPrefixes: MutableList = mutableListOf().asObservable().apply {
        addAll(listOf("ver-base-", "version-base-"))
        registerElementAddedHandler { project.calculateAutoVcsVersion() }
    }

    fun baseVersionTagPrefix(baseVersionTagPrefix: String) = changeSettings {
        baseVersionTagPrefixes.add(baseVersionTagPrefix)
    }

    fun baseVersionTagPrefixes(baseVersionTagPrefixes: Iterable) = changeSettings {
        this.baseVersionTagPrefixes.addAll(versionTagPrefixes)
    }

    fun baseVersionTagPrefixes(vararg baseVersionTagPrefixes: String) = changeSettings {
        this.baseVersionTagPrefixes.addAll(baseVersionTagPrefixes)
    }

    fun setBaseVersionTagPrefixes(baseVersionTagPrefixes: Collection) = changeSettings {
        if (baseVersionTagPrefixes.isEmpty()) throw IllegalArgumentException("baseVersionTagPrefixes can't be empty")
        this.baseVersionTagPrefixes.apply {
            clear()
            addAll(baseVersionTagPrefixes)
        }
    }

    fun setBaseVersionTagPrefixes(vararg baseVersionTagPrefixes: String) = setBaseVersionTagPrefixes(baseVersionTagPrefixes.toList())


    var incrementVersionByCommitsCount: Boolean = false
        set(value) {
            if (field != value) {
                field = value
                project.calculateAutoVcsVersion()
            }
        }


    var incrementVersionBy: Int = 0
        set(value) {
            if (field != value) {
                field = value
                project.calculateAutoVcsVersion()
            }
        }


    var useLatestVersion: Boolean = false
        set(value) {
            if (field != value) {
                field = value
                project.calculateAutoVcsVersion()
            }
        }


    internal var currentMutable: CurrentAutoVcsVersionInfo? = null
    val current: CurrentAutoVcsVersionInfo
        get() {
            currentMutable?.let { return it }
            currentMutable = CurrentAutoVcsVersionInfo()
            project.calculateAutoVcsVersion()
            return currentMutable!!
        }

}


class CurrentAutoVcsVersionInfo {

    var version: Version = Version.create(0, 0, 0)
        internal set

    var latestVersion: Version = Version.create(0, 0, 0)
        internal set

    var commitsAfterLatestVersion: Int = 0
        internal set

    var increment: Int = 0
        internal set

    var latestBaseVersion: Version? = null
        internal set

    var useLatestVersion: Boolean = false
        internal set

}


fun Project.calculateAutoVcsVersion() = calculateAutoVcsVersion(true)

private fun Project.calculateAutoVcsVersion(checkIfAutoVcsVersionPluginApplied: Boolean) {
    if (!checkIfAutoVcsVersionPluginApplied || isPluginAppliedAndNotDisabled(AutoVcsVersionPlugin::class.java)) {
        this.extensions
            .let { it.findByType(AutoVcsVersionChanger::class.java) ?: it.createWithAutoName(AutoVcsVersionChanger::class.java, this) }
            .invoke()
    }
}

@KotlinAllOpen
class AutoVcsVersionChanger(private val project: Project) : () -> Unit {

    companion object {
        private val logger = getGradleLogger(AutoVcsVersionChanger::class.java)
        private val REPEATED_SNAPSHOT_REGEX = Regex("-SNAPSHOT(.*-SNAPSHOT)", IGNORE_CASE)
    }

    private val autoVcsVersion = project[AutoVcsVersionExtension::class.java]
    private val vcsOperations = project[VcsOperationsExtension::class.java]

    private val latestVersionTagWithDepthCache: MutableMap>, TagsWithDepth> = concurrentMapOf()
    private val latestBaseVersionTagNameCache: MutableMap>, Set> = concurrentMapOf()

    @Suppress("ComplexMethod", "LongMethod")
    override fun invoke() {
        val vcsCacheKey = vcsOperations.calculateCacheKey()

        val versionTagPrefixes = autoVcsVersion.versionTagPrefixes.sortedDescending()
        val latestVersionTagWithDepth = latestVersionTagWithDepthCache.computeIfAbsent(Pair(vcsCacheKey, versionTagPrefixes)) {
            vcsOperations.findTagWithDepth { tagName -> it.second.any { tagName.startsWith(it) && null != Version.parseOrNull(tagName.substring(it.length)) } }
                ?: TagsWithDepth(emptySet(), 0)
        }
        val latestVersion: Version = latestVersionTagWithDepth.tagNames.stream()
            .map { tagName ->
                val prefix = versionTagPrefixes.first { tagName.startsWith(it) && null != Version.parseOrNull(tagName.substring(it.length)) }
                return@map Version.parse(tagName.substring(prefix.length)).withoutSuffix()
            }
            .max()
            ?: Version.create(0, 0, 0)
        val commitsAfterLatestVersion = latestVersionTagWithDepth.depth

        val baseVersionTagPrefixes = autoVcsVersion.baseVersionTagPrefixes.sortedDescending()
        val latestBaseVersionTagNames = latestBaseVersionTagNameCache.computeIfAbsent(Pair(vcsCacheKey, baseVersionTagPrefixes)) {
            vcsOperations.getAllTagNames().filterTo(mutableSetOf()) { tagName -> it.second.any { tagName.startsWith(it) && null != Version.parseOrNull(tagName.substring(it.length)) } }
        }
        val latestBaseVersion: Version?
        if (latestBaseVersionTagNames.isNotEmpty()) {
            latestBaseVersion = latestBaseVersionTagNames.stream()
                .map { tagName ->
                    val prefix = baseVersionTagPrefixes.first { tagName.startsWith(it) && null != Version.parseOrNull(tagName.substring(it.length)) }
                    return@map Version.parse(tagName.substring(prefix.length)).withoutSuffix()
                }
                .max()
        } else {
            latestBaseVersion = null
        }

        val useLatestVersion = autoVcsVersion.useLatestVersion
        val incrementVersionByCommitsCount = autoVcsVersion.incrementVersionByCommitsCount
        var version: Version = run {
            if (latestBaseVersion == null || latestBaseVersion <= latestVersion) {
                return@run latestVersion.incrementNumber(
                    latestVersion.numbersCount - 1,
                    commitsAfterLatestVersion.let { commits ->
                        if (useLatestVersion) {
                            0
                        } else if (commits <= 1 || incrementVersionByCommitsCount) {
                            commits
                        } else {
                            1
                        }
                    }
                )
            } else {
                return@run latestBaseVersion.withNumber(latestBaseVersion.numbersCount, 0)
            }
        }

        val incrementVersionBy = autoVcsVersion.incrementVersionBy
        if (incrementVersionBy > 0) {
            version = version.incrementNumber(latestVersion.numbersCount - 1, incrementVersionBy)
        }

        var isSnapshot = false
        vcsOperations.currentBranch?.let { currentBranch ->
            if (vcsOperations.masterBranch != currentBranch) {
                isSnapshot = true
                val branchSlug = VcsOperations.createBranchSlug(currentBranch)
                if (autoVcsVersion.incrementVersionByCommitsCount) {
                    val time = LocalDateTime.now().toEpochSecond(UTC)
                    version = version.appendSuffix("$branchSlug-$time", "-")
                } else {
                    version = version.appendSuffix(branchSlug, "-")
                }
            }
        }

        if (isSnapshot) {
            version = version.appendSuffix("SNAPSHOT", "-")
        }

        while (REPEATED_SNAPSHOT_REGEX.containsMatchIn(version.suffix)) {
            version = version.withSuffix(REPEATED_SNAPSHOT_REGEX.replace(version.suffix, "\$1"))
        }


        project.rootProject.allprojects.asSequence()
            .flatMap { it.tasks.asSequence() }
            .filterIsInstance(WriteAutoVcsVersionInFileTask::class.java)
            .map(WriteAutoVcsVersionInFileTask::file)
            .map(File::getAbsoluteFile)
            .filter(File::isFile)
            .firstOrNull()
            ?.let { versionFile ->
                val versionInFile = try {
                    Version.parse(versionFile.readText().trim())
                } catch (e: VersionParsingException) {
                    logger.warn(e.toString())
                    return@let
                }

                logger.lifecycle("Version '{}' is read from file: {}", versionInFile, versionFile)

                version = versionInFile
            }


        project[AutoVcsVersionExtension::class.java].current.also {
            it.version = version
            it.latestVersion = latestVersion
            it.commitsAfterLatestVersion = commitsAfterLatestVersion
            it.increment = incrementVersionBy
            it.latestBaseVersion = latestBaseVersion
            it.useLatestVersion = useLatestVersion
        }


        project.setAllprojectsVersion(version.toString())
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy