From cfa252f218a640c78935b3cb064f68e1a38ffced Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Thu, 14 Mar 2024 13:06:33 +0100 Subject: Move changelog files into package --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 41 ++- gradlew.bat | 15 +- src/main/kotlin/Main.kt | 347 --------------------- src/main/kotlin/PullRequest.kt | 141 --------- .../kotlin/at/hannibal2/skyhanni/changelog/Main.kt | 347 +++++++++++++++++++++ .../at/hannibal2/skyhanni/changelog/PullRequest.kt | 141 +++++++++ 8 files changed, 532 insertions(+), 507 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100644 src/main/kotlin/Main.kt delete mode 100644 src/main/kotlin/PullRequest.kt create mode 100644 src/main/kotlin/at/hannibal2/skyhanni/changelog/Main.kt create mode 100644 src/main/kotlin/at/hannibal2/skyhanni/changelog/PullRequest.kt diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt deleted file mode 100644 index 8f62f34..0000000 --- a/src/main/kotlin/Main.kt +++ /dev/null @@ -1,347 +0,0 @@ -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 (?.*)".toPattern() -val changePattern = "\\+ (?.*) - (?.*)".toPattern() -val extraInfoPattern = " {4}\\* (?.*)".toPattern() -val illegalStartPattern = "^[-=*+ ].*".toPattern() - -fun getTextFromUrl(urlString: String): List { - val url = URL(urlString) - val connection = url.openConnection() - val inputStream = connection.getInputStream() - val text = mutableListOf() - - 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, - firstPr: Int, - hideWhenError: Boolean, - whatToDo: WhatToDo, - fullVersion: String, - beta: Int, -) { - val allChanges = mutableListOf() - val errors = mutableListOf() - 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): 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, - 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, - extraInfoPrefix: String, - fullVersion: String, - beta: Int, -): MutableList { - val list = mutableListOf() - 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 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, - prLink: String, -): List { - var currentCategory: Category? = null - var currentChange: Change? = null - val changes = mutableListOf() - 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() -} diff --git a/src/main/kotlin/PullRequest.kt b/src/main/kotlin/PullRequest.kt deleted file mode 100644 index cd9122f..0000000 --- a/src/main/kotlin/PullRequest.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.example - -import com.google.gson.annotations.SerializedName - -data class PullRequest( - val url: String, - val id: Long, - @SerializedName("node_id") val nodeId: String, - @SerializedName("html_url") val htmlUrl: String, - @SerializedName("diff_url") val diffUrl: String, - @SerializedName("patch_url") val patchUrl: String, - @SerializedName("issue_url") val issueUrl: String, - val number: Int, - val state: String, - val locked: Boolean, - val title: String, - val user: User, - val body: String?, - @SerializedName("created_at") val createdAt: String, - @SerializedName("updated_at") val updatedAt: String, - @SerializedName("closed_at") val closedAt: String?, - @SerializedName("merged_at") val mergedAt: String?, - @SerializedName("merge_commit_sha") val mergeCommitSha: String?, - val assignee: Any?, // Change type if you have specific assignee structure - val assignees: List, // Change type if you have specific assignees structure - @SerializedName("requested_reviewers") val requestedReviewers: List, // Change type if you have specific requested reviewers structure - @SerializedName("requested_teams") val requestedTeams: List, // Change type if you have specific requested teams structure - val labels: List, // Change type if you have specific labels structure - val milestone: Milestone, - val draft: Boolean, - @SerializedName("commits_url") val commitsUrl: String, - @SerializedName("review_comments_url") val reviewCommentsUrl: String, - @SerializedName("review_comment_url") val reviewCommentUrl: String, - @SerializedName("comments_url") val commentsUrl: String, - @SerializedName("statuses_url") val statusesUrl: String, - val head: Head, - @SerializedName("author_association") val authorAssociation: String, - @SerializedName("auto_merge") val autoMerge: Any?, // Change type if you have specific auto merge structure - @SerializedName("active_lock_reason") val activeLockReason: Any? // Change type if you have specific active lock reason structure -) - -data class User( - val login: String, - val id: Long, - @SerializedName("node_id") val nodeId: String, - @SerializedName("avatar_url") val avatarUrl: String, - @SerializedName("html_url") val htmlUrl: String, - @SerializedName("followers_url") val followersUrl: String, - @SerializedName("following_url") val followingUrl: String, - @SerializedName("gists_url") val gistsUrl: String, - @SerializedName("starred_url") val starredUrl: String, - @SerializedName("subscriptions_url") val subscriptionsUrl: String, - @SerializedName("organizations_url") val organizationsUrl: String, - @SerializedName("repos_url") val reposUrl: String, - @SerializedName("events_url") val eventsUrl: String, - @SerializedName("received_events_url") val receivedEventsUrl: String, - val type: String, - @SerializedName("site_admin") val siteAdmin: Boolean -) - -data class Milestone( - val url: String, - @SerializedName("html_url") val htmlUrl: String, - @SerializedName("labels_url") val labelsUrl: String, - val id: Long, - @SerializedName("node_id") val nodeId: String, - val number: Int, - val title: String, - val description: String?, - val creator: User, - @SerializedName("open_issues") val openIssues: Int, - @SerializedName("closed_issues") val closedIssues: Int, - val state: String, - @SerializedName("created_at") val createdAt: String, - @SerializedName("updated_at") val updatedAt: String, - @SerializedName("due_on") val dueOn: String?, - @SerializedName("closed_at") val closedAt: String? -) - -data class Head( - val label: String, - val ref: String, - val sha: String, - val user: User, - val repo: Repo -) - -data class Repo( - val id: Long, - @SerializedName("node_id") val nodeId: String, - val name: String, - @SerializedName("full_name") val fullName: String, - val private: Boolean, - val owner: User, - @SerializedName("html_url") val htmlUrl: String, - val description: String, - val fork: Boolean, - val url: String, - @SerializedName("forks_url") val forksUrl: String, - @SerializedName("keys_url") val keysUrl: String, - @SerializedName("collaborators_url") val collaboratorsUrl: String, - @SerializedName("teams_url") val teamsUrl: String, - @SerializedName("hooks_url") val hooksUrl: String, - @SerializedName("issue_events_url") val issueEventsUrl: String, - @SerializedName("events_url") val eventsUrl: String, - @SerializedName("assignees_url") val assigneesUrl: String, - @SerializedName("branches_url") val branchesUrl: String, - @SerializedName("tags_url") val tagsUrl: String, - @SerializedName("blobs_url") val blobsUrl: String, - @SerializedName("git_tags_url") val gitTagsUrl: String, - @SerializedName("git_refs_url") val gitRefsUrl: String, - @SerializedName("trees_url") val treesUrl: String, - @SerializedName("statuses_url") val statusesUrl: String, - @SerializedName("languages_url") val languagesUrl: String, - @SerializedName("stargazers_url") val stargazersUrl: String, - @SerializedName("contributors_url") val contributorsUrl: String, - @SerializedName("subscribers_url") val subscribersUrl: String, - @SerializedName("subscription_url") val subscriptionUrl: String, - @SerializedName("commits_url") val commitsUrl: String, - @SerializedName("git_commits_url") val gitCommitsUrl: String, - @SerializedName("comments_url") val commentsUrl: String, - @SerializedName("issue_comment_url") val issueCommentUrl: String, - @SerializedName("contents_url") val contentsUrl: String, - @SerializedName("compare_url") val compareUrl: String, - @SerializedName("merges_url") val mergesUrl: String, - @SerializedName("archive_url") val archiveUrl: String, - @SerializedName("downloads_url") val downloadsUrl: String, - @SerializedName("issues_url") val issuesUrl: String, - @SerializedName("pulls_url") val pullsUrl: String, - @SerializedName("milestones_url") val milestonesUrl: String, - @SerializedName("notifications_url") val notificationsUrl: String, - @SerializedName("labels_url") val labelsUrl: String, - @SerializedName("releases_url") val releasesUrl: String, - @SerializedName("deployments_url") val deploymentsUrl: String, - @SerializedName("created_at") val createdAt: String, - @SerializedName("updated_at") val updatedAt: String, - @SerializedName("pushed_at") val pushedAt: String, - @SerializedName("git_url") val gitUrl: String, - @SerializedName("ssh_url") val sshUrl: String, - @SerializedName("clone_url") val cloneUrl: String, -) \ No newline at end of file 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 (?.*)".toPattern() +val changePattern = "\\+ (?.*) - (?.*)".toPattern() +val extraInfoPattern = " {4}\\* (?.*)".toPattern() +val illegalStartPattern = "^[-=*+ ].*".toPattern() + +fun getTextFromUrl(urlString: String): List { + val url = URL(urlString) + val connection = url.openConnection() + val inputStream = connection.getInputStream() + val text = mutableListOf() + + 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, + firstPr: Int, + hideWhenError: Boolean, + whatToDo: WhatToDo, + fullVersion: String, + beta: Int, +) { + val allChanges = mutableListOf() + val errors = mutableListOf() + 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): 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, + 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, + extraInfoPrefix: String, + fullVersion: String, + beta: Int, +): MutableList { + val list = mutableListOf() + 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 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, + prLink: String, +): List { + var currentCategory: Category? = null + var currentChange: Change? = null + val changes = mutableListOf() + 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() +} diff --git a/src/main/kotlin/at/hannibal2/skyhanni/changelog/PullRequest.kt b/src/main/kotlin/at/hannibal2/skyhanni/changelog/PullRequest.kt new file mode 100644 index 0000000..cd9122f --- /dev/null +++ b/src/main/kotlin/at/hannibal2/skyhanni/changelog/PullRequest.kt @@ -0,0 +1,141 @@ +package org.example + +import com.google.gson.annotations.SerializedName + +data class PullRequest( + val url: String, + val id: Long, + @SerializedName("node_id") val nodeId: String, + @SerializedName("html_url") val htmlUrl: String, + @SerializedName("diff_url") val diffUrl: String, + @SerializedName("patch_url") val patchUrl: String, + @SerializedName("issue_url") val issueUrl: String, + val number: Int, + val state: String, + val locked: Boolean, + val title: String, + val user: User, + val body: String?, + @SerializedName("created_at") val createdAt: String, + @SerializedName("updated_at") val updatedAt: String, + @SerializedName("closed_at") val closedAt: String?, + @SerializedName("merged_at") val mergedAt: String?, + @SerializedName("merge_commit_sha") val mergeCommitSha: String?, + val assignee: Any?, // Change type if you have specific assignee structure + val assignees: List, // Change type if you have specific assignees structure + @SerializedName("requested_reviewers") val requestedReviewers: List, // Change type if you have specific requested reviewers structure + @SerializedName("requested_teams") val requestedTeams: List, // Change type if you have specific requested teams structure + val labels: List, // Change type if you have specific labels structure + val milestone: Milestone, + val draft: Boolean, + @SerializedName("commits_url") val commitsUrl: String, + @SerializedName("review_comments_url") val reviewCommentsUrl: String, + @SerializedName("review_comment_url") val reviewCommentUrl: String, + @SerializedName("comments_url") val commentsUrl: String, + @SerializedName("statuses_url") val statusesUrl: String, + val head: Head, + @SerializedName("author_association") val authorAssociation: String, + @SerializedName("auto_merge") val autoMerge: Any?, // Change type if you have specific auto merge structure + @SerializedName("active_lock_reason") val activeLockReason: Any? // Change type if you have specific active lock reason structure +) + +data class User( + val login: String, + val id: Long, + @SerializedName("node_id") val nodeId: String, + @SerializedName("avatar_url") val avatarUrl: String, + @SerializedName("html_url") val htmlUrl: String, + @SerializedName("followers_url") val followersUrl: String, + @SerializedName("following_url") val followingUrl: String, + @SerializedName("gists_url") val gistsUrl: String, + @SerializedName("starred_url") val starredUrl: String, + @SerializedName("subscriptions_url") val subscriptionsUrl: String, + @SerializedName("organizations_url") val organizationsUrl: String, + @SerializedName("repos_url") val reposUrl: String, + @SerializedName("events_url") val eventsUrl: String, + @SerializedName("received_events_url") val receivedEventsUrl: String, + val type: String, + @SerializedName("site_admin") val siteAdmin: Boolean +) + +data class Milestone( + val url: String, + @SerializedName("html_url") val htmlUrl: String, + @SerializedName("labels_url") val labelsUrl: String, + val id: Long, + @SerializedName("node_id") val nodeId: String, + val number: Int, + val title: String, + val description: String?, + val creator: User, + @SerializedName("open_issues") val openIssues: Int, + @SerializedName("closed_issues") val closedIssues: Int, + val state: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("updated_at") val updatedAt: String, + @SerializedName("due_on") val dueOn: String?, + @SerializedName("closed_at") val closedAt: String? +) + +data class Head( + val label: String, + val ref: String, + val sha: String, + val user: User, + val repo: Repo +) + +data class Repo( + val id: Long, + @SerializedName("node_id") val nodeId: String, + val name: String, + @SerializedName("full_name") val fullName: String, + val private: Boolean, + val owner: User, + @SerializedName("html_url") val htmlUrl: String, + val description: String, + val fork: Boolean, + val url: String, + @SerializedName("forks_url") val forksUrl: String, + @SerializedName("keys_url") val keysUrl: String, + @SerializedName("collaborators_url") val collaboratorsUrl: String, + @SerializedName("teams_url") val teamsUrl: String, + @SerializedName("hooks_url") val hooksUrl: String, + @SerializedName("issue_events_url") val issueEventsUrl: String, + @SerializedName("events_url") val eventsUrl: String, + @SerializedName("assignees_url") val assigneesUrl: String, + @SerializedName("branches_url") val branchesUrl: String, + @SerializedName("tags_url") val tagsUrl: String, + @SerializedName("blobs_url") val blobsUrl: String, + @SerializedName("git_tags_url") val gitTagsUrl: String, + @SerializedName("git_refs_url") val gitRefsUrl: String, + @SerializedName("trees_url") val treesUrl: String, + @SerializedName("statuses_url") val statusesUrl: String, + @SerializedName("languages_url") val languagesUrl: String, + @SerializedName("stargazers_url") val stargazersUrl: String, + @SerializedName("contributors_url") val contributorsUrl: String, + @SerializedName("subscribers_url") val subscribersUrl: String, + @SerializedName("subscription_url") val subscriptionUrl: String, + @SerializedName("commits_url") val commitsUrl: String, + @SerializedName("git_commits_url") val gitCommitsUrl: String, + @SerializedName("comments_url") val commentsUrl: String, + @SerializedName("issue_comment_url") val issueCommentUrl: String, + @SerializedName("contents_url") val contentsUrl: String, + @SerializedName("compare_url") val compareUrl: String, + @SerializedName("merges_url") val mergesUrl: String, + @SerializedName("archive_url") val archiveUrl: String, + @SerializedName("downloads_url") val downloadsUrl: String, + @SerializedName("issues_url") val issuesUrl: String, + @SerializedName("pulls_url") val pullsUrl: String, + @SerializedName("milestones_url") val milestonesUrl: String, + @SerializedName("notifications_url") val notificationsUrl: String, + @SerializedName("labels_url") val labelsUrl: String, + @SerializedName("releases_url") val releasesUrl: String, + @SerializedName("deployments_url") val deploymentsUrl: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("updated_at") val updatedAt: String, + @SerializedName("pushed_at") val pushedAt: String, + @SerializedName("git_url") val gitUrl: String, + @SerializedName("ssh_url") val sshUrl: String, + @SerializedName("clone_url") val cloneUrl: String, +) \ No newline at end of file -- cgit