diff options
Diffstat (limited to 'src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt')
-rw-r--r-- | src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt b/src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt new file mode 100644 index 0000000..8f62f34 --- /dev/null +++ b/src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt @@ -0,0 +1,347 @@ +package org.example + +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray + +import java.net.URL +import java.time.Instant +import java.util.regex.Matcher +import java.util.regex.Pattern +import kotlin.system.exitProcess + +enum class Category(val changeLogName: String, val prTitle: String) { + NEW("New Features", "Feature"), + IMPROVEMENT("Improvements", "Improvement"), + FIX("Fixes", "Fix"), + INTERNAL("Technical Details", "Backend"), + REMOVED("Removed Features", "Removed Feature"), + ; +} + +//val allowedCategories = listOf("New Features", "Improvements", "Fixes", "Technical Details", "Removed Features") + +val categoryPattern = "## Changelog (?<category>.*)".toPattern() +val changePattern = "\\+ (?<text>.*) - (?<author>.*)".toPattern() +val extraInfoPattern = " {4}\\* (?<text>.*)".toPattern() +val illegalStartPattern = "^[-=*+ ].*".toPattern() + +fun getTextFromUrl(urlString: String): List<String> { + val url = URL(urlString) + val connection = url.openConnection() + val inputStream = connection.getInputStream() + val text = mutableListOf<String>() + + inputStream.bufferedReader().useLines { lines -> + lines.forEach { + text.add(it) + } + } + + return text +} + +enum class WhatToDo { + NEXT_BETA, OPEN_PRS, + ; +} + +fun main() { + val firstPr = 1168 + val hideWhenError = true + val fullVersion = "0.24" + val beta = 13 + + val whatToDo = WhatToDo.NEXT_BETA + + @Suppress("KotlinConstantConditions") + val url = when (whatToDo) { + WhatToDo.NEXT_BETA -> "https://api.github.com/repos/hannibal002/SkyHanni/pulls?state=closed&sort=updated&direction=desc&per_page=50" + WhatToDo.OPEN_PRS -> "https://api.github.com/repos/hannibal002/SkyHanni/pulls?state=open&sort=updated&direction=desc&per_page=30" + } + + val data = getTextFromUrl(url).joinToString("") + val gson = GsonBuilder().create() + val fromJson = gson.fromJson(data, JsonArray::class.java) + val prs = fromJson.map { gson.fromJson(it, PullRequest::class.java) } + readPrs(prs, firstPr, hideWhenError, whatToDo, fullVersion, beta) +} + +fun readPrs( + prs: List<PullRequest>, + firstPr: Int, + hideWhenError: Boolean, + whatToDo: WhatToDo, + fullVersion: String, + beta: Int, +) { + val allChanges = mutableListOf<Change>() + val errors = mutableListOf<String>() + var excluded = 0 + var done = 0 + var wrongPrName = 0 + // TODO find better solution for this sorting logic + val filtered = when (whatToDo) { + WhatToDo.NEXT_BETA -> prs.filter { it.mergedAt != null } + .map { it to it.mergedAt } + + WhatToDo.OPEN_PRS -> prs + .map { it to it.updatedAt } + + } + .map { it.first to Long.MAX_VALUE - Instant.parse(it.second).toEpochMilli() } + .sortedBy { it.second } + .map { it.first } + + println("") + for (pr in filtered) { + val number = pr.number + val prLink = pr.htmlUrl + val body = pr.body + val title = pr.title + + val description = body?.split(System.lineSeparator()) ?: emptyList() + if (description.isNotEmpty()) { + val last = description.last() + if (last == "exclude_from_changelog") { + println("") + println("Excluded #$number ($prLink)") + excluded++ + continue + } + } + try { + val newChanges = parseChanges(description, prLink) + if (hasWrongPrName(prLink, title, newChanges)) { + wrongPrName++ + } + allChanges.addAll(newChanges) + done++ + } catch (t: Throwable) { + errors.add("Error in #$number ($prLink)\n${t.message}") + if (hasWrongPrName(prLink, title, emptyList())) { + wrongPrName++ + } + } + if (whatToDo == WhatToDo.NEXT_BETA) { + if (number == firstPr) break + } + } + println("") + + for (error in errors) { + println(" ") + println(error) + } + + for (type in OutputType.entries) { + print(allChanges, type, fullVersion, beta) + } + println("") + if (excluded > 0) { + println("Excluded $excluded PRs.") + } + val errorSize = errors.size + if (errorSize > 0) { + println("Found $errorSize PRs with errors!") + } + if (wrongPrName > 0) { + println("Found $wrongPrName PRs with wrong names!") + } + println("Loaded $done PRs correctly.") + if (errorSize > 0) { + if (hideWhenError) { + exitProcess(-1) + } + } +} + +fun hasWrongPrName(prLink: String, title: String, newChanges: List<Change>): Boolean { + val hasFix = newChanges.any { it.category == Category.FIX } + for (category in Category.entries) { + if (newChanges.any { it.category == category }) { + var start = category.prTitle + if (hasFix && category != Category.FIX) { + start += " + Fix" + } + start += ": " + val wrongName = !title.startsWith(start) + if (wrongName) { + println("wrong pr title!") + println("found: '$title'") + println("should start with $start") + println("link: $prLink") + println(" ") + } + return wrongName + } + } + + val prefix = "Wrong/broken Changelog: " + if (!title.startsWith(prefix)) { + println("wrong pr title!") + println("found: '$title'") + println("should start with $prefix") + println("link: $prLink") + println(" ") + return true + } + + return false +} + +enum class OutputType { + DISCORD_INTERNAL, GITHUB, DISCORD_PUBLIC, +} + +private fun print( + allChanges: MutableList<Change>, + outputType: OutputType, + fullVersion: String, + beta: Int, +) { + val extraInfoPrefix = when (outputType) { + OutputType.DISCORD_PUBLIC -> " = " + OutputType.GITHUB -> " * " + OutputType.DISCORD_INTERNAL -> " - " + } + val list = createPrint(outputType, allChanges, extraInfoPrefix, fullVersion, beta) + val border = "=================================================================================" + println("") + println("outputType ${outputType.name.lowercase()}:") + val totalLength = list.sumOf { it.length } + if (outputType != OutputType.GITHUB) { + println("$totalLength/2000 characters used") + } + println(border) + for (line in list) { + println(line) + } + println(border) +} + +private fun createPrint( + outputType: OutputType, + allChanges: MutableList<Change>, + extraInfoPrefix: String, + fullVersion: String, + beta: Int, +): MutableList<String> { + val list = mutableListOf<String>() + list.add("## Version $fullVersion Beta $beta") + + for (category in Category.entries) { + if (outputType == OutputType.DISCORD_PUBLIC && category == Category.INTERNAL) continue + val changes = allChanges.filter { it.category == category } + if (changes.isEmpty()) continue + list.add("### " + category.changeLogName) + if (outputType == OutputType.DISCORD_PUBLIC) { + list.add("```diff") + } + for (change in changes) { + val pr = when (outputType) { + OutputType.DISCORD_PUBLIC -> "" + OutputType.GITHUB -> " (${change.prLink})" + OutputType.DISCORD_INTERNAL -> " [PR](<${change.prLink}>)" + } + val changePrefix = getChangePrefix(category, outputType) + list.add("$changePrefix${change.text} - ${change.author}$pr") + for (s in change.extraInfo) { + list.add("$extraInfoPrefix$s") + } + } + if (outputType == OutputType.DISCORD_PUBLIC) { + list.add("```") + } + } + if (outputType == OutputType.DISCORD_PUBLIC) { + val root = "https://github.com/hannibal002/SkyHanni" + val releaseLink = "$root/releases/tag/$fullVersion.Beta.$beta>" + list.add("For a full changelog, including technical details, see the [GitHub release](<$releaseLink)") + + val downloadLink = "$root/releases/download/$fullVersion.Beta.$beta/SkyHanni-$fullVersion.Beta.$beta.jar" + list.add("Download link: $downloadLink") + } + + return list +} + +fun getChangePrefix(category: Category, outputType: OutputType): String = when (outputType) { + OutputType.DISCORD_INTERNAL -> "- " + OutputType.GITHUB -> "+ " + OutputType.DISCORD_PUBLIC -> when (category) { + Category.NEW -> "+ " + Category.IMPROVEMENT -> "+ " + Category.FIX -> "~ " + Category.REMOVED -> "- " + Category.INTERNAL -> error("internal not in discord public") + } +} + +inline fun <T> Pattern.matchMatcher(text: String, consumer: Matcher.() -> T) = + matcher(text).let { if (it.matches()) consumer(it) else null } + +@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") +fun parseChanges( + description: List<String>, + prLink: String, +): List<Change> { + var currentCategory: Category? = null + var currentChange: Change? = null + val changes = mutableListOf<Change>() + for (line in description) { + if (line == "") { + currentChange = null + currentCategory = null + continue + } + + categoryPattern.matchMatcher(line) { + val categoryName = group("category") + currentCategory = getCategoryByLogName(categoryName) ?: error("unknown category: '$categoryName'") + currentChange = null + continue + } + + val category = currentCategory ?: continue + + changePattern.matchMatcher(line) { + val author = group("author") + if (author == "your_name_here") { + error("no author name") + } + val text = group("text") + if (illegalStartPattern.matcher(text).matches()) { + error("illegal start at change: '$text'") + } + currentChange = Change(text, category, prLink, author).also { + changes.add(it) + } + continue + } + + extraInfoPattern.matchMatcher(line) { + val change = currentChange ?: error("Found extra info without change: '$line'") + val text = group("text") + if (illegalStartPattern.matcher(text).matches()) { + error("illegal start at extra info: '$text'") + } + change.extraInfo.add(text) + continue + } + error("found unexpected line: '$line'") + } + + if (changes.isEmpty()) { + error("no changes found") + } + + return changes +} + +fun getCategoryByLogName(name: String): Category? = Category.entries.find { it.changeLogName == name } + +//class Category(val name: String) + +class Change(val text: String, val category: Category, val prLink: String, val author: String) { + val extraInfo = mutableListOf<String>() +} |