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

gradle-plugins.changelog.0.1.0-rc.4.source-code.AddChangelogItem.kt Maven / Gradle / Ivy

There is a newer version: 0.1.0-rc.45
Show newest version
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option

abstract class AddChangelogItem : DefaultTask() {

    @get:Input
    @set:Option(option = "added", description = "Add an item to the `added` section")
    @Optional
    var added: String? = null

    @get:Input
    @set:Option(option = "changed", description = "Add an item to the `changed` section")
    @Optional
    var changed: String? = null

    @get:Input
    @set:Option(option = "deprecated", description = "Add an item to the `deprecated` section")
    @Optional
    var deprecated: String? = null

    @get:Input
    @set:Option(option = "removed", description = "Add an item to the `removed` section")
    @Optional
    var removed: String? = null

    @get:Input
    @set:Option(option = "fixed", description = "Add an item to the `fixed` section")
    @Optional
    var fixed: String? = null

    @get:Input
    @set:Option(option = "updated", description = "Add an item to the `updated` section")
    @Optional
    var updated: String? = null

    @get:Input
    @set:Option(
        option = "renovate",
        description = "Extract dependencies from the table in the PR body",
    )
    @Optional
    var renovate: String? = null

    @get:Input
    @set:Option(
        option = "renovatePath",
        description = "Extract dependencies from the table in the PR body from a file",
    )
    @Optional
    var renovatePath: String? = null

    @get:Input
    @set:Option(
        option = "renovateCommitTable",
        description =
            """
               Extract dependencies from the table in the commit body
               Add `"commitBodyTable": true` to the Renovate config
            """,
    )
    var renovateCommitTable: Boolean = false

    init {
        group = "changelog"
    }

    @TaskAction
    fun run() {
        check(project.changelogFile.exists()) { "CHANGELOG.md file doesn't found" }
        setupSection("### Added", added)
        setupSection("### Changed", changed)
        setupSection("### Deprecated", deprecated)
        setupSection("### Removed", removed)
        setupSection("### Fixed", fixed)
        setupSection("### Updated", updated)
        setupRenovate()
    }
}

private val Project.changelog: String
    get() = changelogFile.readText()

private fun AddChangelogItem.setupSection(header: String, item: String?) =
    with(project) {
        item?.let { item ->
            logger.lifecycle(header)
            logger.lifecycle("- $item")
            changelogFile.writeText(changelog.addChanges(header, listOf(item)))
        }
    }

private fun AddChangelogItem.setupRenovate(): Unit =
    with(project) {
        val dependenciesFromPullRequest: List =
            dependenciesFromRenovatePullRequestBody(renovate, renovatePath)

        val dependenciesFromCommit: List =
            if (renovateCommitTable) dependenciesFromRenovateCommit() else emptyList()

        val updatedLabel = "### Updated"

        when {
            dependenciesFromPullRequest.isNotEmpty() -> {
                logger.lifecycle(updatedLabel)
                for (dependencyFromPullRequest in dependenciesFromPullRequest) {
                    logger.lifecycle("- $dependencyFromPullRequest")
                }
                changelogFile.writeText(
                    changelog.addChanges(updatedLabel, dependenciesFromPullRequest),
                )
            }
            dependenciesFromCommit.isNotEmpty() -> {
                logger.lifecycle(updatedLabel)
                for (dependencyFromCommit in dependenciesFromCommit) {
                    logger.lifecycle("- $dependencyFromCommit")
                }
                changelogFile.writeText(changelog.addChanges(updatedLabel, dependenciesFromCommit))
            }
        }
    }

@OptIn(ExperimentalStdlibApi::class)
private fun String.addChanges(header: String, changes: List): String =
    buildList {
            val firstVersionIndex =
                lines().indexOfFirst {
                    it.startsWith("## [") && it.contains("[Unreleased]", true).not()
                }
            var shouldAddUpdate = true
            lines().onEach { line ->
                if (line.startsWith(header) && shouldAddUpdate) {
                    shouldAddUpdate = false
                    add(line)
                    for (change in changes) {
                        if (lines().subList(0, firstVersionIndex).none { it.contains(change) }) {
                            add("- $change")
                        }
                    }
                } else {
                    add(line)
                }
            }
            runCatching {
                forEachIndexed { index: Int, line ->
                    val updateRegex = """(- `)(.*)( )(->)( )(.*)(`)"""
                    if (Regex(updateRegex).matches(line)) {
                        val module =
                            line.filterNot(Char::isWhitespace)
                                .replaceAfter("->", "")
                                .replace("->", "")
                                .drop(1)
                        for (j in index + 1 until firstVersionIndex) {
                            val lineToRemove = this[j]
                            if (lineToRemove.contains(module) &&
                                    Regex(updateRegex).matches(lineToRemove)
                            ) {
                                removeAt(j)
                            }
                        }
                    }
                }
            }
        }
        .joinToString("\n")

private fun Project.dependenciesFromRenovatePullRequestBody(
    body: String?,
    path: String?
): List {
    val renovateLines: List =
        when {
            body != null && body.isNotBlank() -> body.split("""\n""")
            path != null -> File("$rootDir/$path").readText().split("\n")
            else -> emptyList()
        }

    return renovateLines
        .asSequence()
        .filter(String::isNotBlank)
        .map { it.replace("""\n""", "\n") }
        .dropWhile { it.startsWith("| Package | Change |").not() }
        .dropWhile { it.startsWith("|---").not() }
        .drop(1)
        .takeWhile { it.startsWith("| ") }
        .flatMap { it.split("|") }
        .map { it.replace(" ", "") }
        .map { if (it.startsWith("`").not()) it else it.replace("`", "").split("->")[1] }
        .filter(String::isNotBlank)
        .filter { it.startsWith("[!").not() }
        .map { if (it.startsWith("[")) it.drop(1).takeWhile { char -> char != ']' } else it }
        .zipWithNext { a: String, b: String ->
            if (a.first().run { isLetter() || this == '[' }) "`$a -> $b`" else null
        }
        .filterNotNull()
        .toList()
}

private fun Project.dependenciesFromRenovateCommit(): List {
    val gitFolder = File("${rootProject.rootDir}").walkTopDown().first { it.name == ".git" }

    val repository: Repository =
        FileRepositoryBuilder().setGitDir(gitFolder).readEnvironment().findGitDir().build()

    val head = repository.resolve(Constants.HEAD).name

    val commits: List =
        Git(repository).log().add(repository.resolve(head)).call().toList()

    val latestCommit: RevCommit =
        commits.first { commit ->
            listOf("datasource", "package", "from", "to").all { keyword ->
                keyword in commit.fullMessage
            }
        }

    return latestCommit
        .fullMessage
        .lines()
        .dropWhile { it.startsWith("| ----").not() }
        .drop(1)
        .dropLastWhile { it.startsWith("|").not() && it.endsWith("|").not() }
        .map {
            val data = it.filterNot(Char::isWhitespace).split("|").drop(2).dropLast(1)
            "`${data.first()} -> ${data.last()}`"
        }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy