summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/kotlin/Main.kt232
-rw-r--r--src/main/kotlin/PullRequest.kt141
2 files changed, 373 insertions, 0 deletions
diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt
new file mode 100644
index 0000000..7ecd07f
--- /dev/null
+++ b/src/main/kotlin/Main.kt
@@ -0,0 +1,232 @@
+package org.example
+
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonArray
+
+import java.net.URL
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import kotlin.system.exitProcess
+
+val allowedCategories = listOf("New Features", "Improvements", "Fixes", "Technical Details", "Removed Features")
+
+val categoryPattern = "## Changelog (?<category>.*)".toPattern()
+val changePattern = "\\+ (?<text>.*) - (?<author>.*)".toPattern()
+val extraInfoPattern = " {2}\\* (?<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
+}
+
+fun main() {
+ val firstPr = 1071
+ val hideWhenError = true
+
+ println(" ")
+ val url =
+ "https://api.github.com/repos/hannibal002/SkyHanni/pulls?state=closed&sort=updated&direction=desc&per_page=50"
+ 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)
+}
+
+fun readPrs(prs: List<PullRequest>, firstPr: Int, hideWhenError: Boolean) {
+ val categories = mutableListOf<Category>()
+ val allChanges = mutableListOf<Change>()
+ findAllChanges(prs, allChanges, categories, firstPr, hideWhenError)
+
+ for (type in OutputType.entries) {
+ print(categories, allChanges, type)
+ }
+}
+
+enum class OutputType {
+ DISCORD_INTERNAL,
+ GITHUB,
+ DISCORD_PUBLIC,
+}
+
+private fun print(
+ categories: MutableList<Category>,
+ allChanges: MutableList<Change>,
+ outputType: OutputType,
+) {
+ val extraInfoPrefix = when (outputType) {
+ OutputType.DISCORD_PUBLIC -> " = "
+ OutputType.GITHUB -> " * "
+ OutputType.DISCORD_INTERNAL -> " "
+ }
+ println(" ")
+ println(" ")
+ println("outputType: $outputType")
+ println(" ")
+ for (category in allowedCategories.map { getCategory(categories, it) }) {
+ if (outputType == OutputType.DISCORD_PUBLIC && category.name == "Technical Details") continue
+ val changes = allChanges.filter { it.category == category }
+ if (changes.isEmpty()) continue
+ println("### " + category.name)
+ if (outputType == OutputType.DISCORD_PUBLIC) {
+ println("```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.name, outputType)
+ println("$changePrefix${change.text} - ${change.author}$pr")
+ for (s in change.extraInfo) {
+ println("$extraInfoPrefix$s")
+ }
+ }
+ if (outputType == OutputType.DISCORD_PUBLIC) {
+ println("```")
+ }
+ }
+}
+
+fun getChangePrefix(name: String, outputType: OutputType): String {
+ return when (outputType) {
+ OutputType.DISCORD_INTERNAL -> " "
+ OutputType.GITHUB -> " + "
+ OutputType.DISCORD_PUBLIC -> when (name) {
+ "New Features" -> "+ "
+ "Improvements" -> "+ "
+ "Fixes" -> "~ "
+ "Removed Features" -> "- "
+ else -> error("impossible change prefix")
+ }
+ }
+}
+
+private fun findAllChanges(
+ prs: List<PullRequest>,
+ changes: MutableList<Change>,
+ categories: MutableList<Category>,
+ firstPr: Int,
+ hideWhenError: Boolean,
+) {
+ var errors = 0
+ var done = 0
+ for (pr in prs) {
+ val number = pr.number
+ val prLink = pr.htmlUrl
+ val body = pr.body
+ val merged = pr.mergedAt != null
+ if (!merged) continue
+
+ val description = body?.split(System.lineSeparator()) ?: emptyList()
+ try {
+ changes.addAll(parseChanges(description, prLink, categories))
+ done++
+ } catch (t: Throwable) {
+ println("")
+ println("Error in #$number ($prLink)")
+ println(t.message)
+ errors++
+ }
+ if (number == firstPr) break
+ }
+ println("")
+ println("found $errors errors")
+ if (errors > 0) {
+ if (hideWhenError) {
+ exitProcess(-1)
+ }
+ }
+ println("Loaded $done PRs")
+}
+
+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,
+ categories: MutableList<Category>,
+): 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")
+ if (categoryName !in allowedCategories) {
+ error("unknown category: '$categoryName'")
+ }
+ currentCategory = getCategory(categories, categoryName)
+ currentChange = null
+ continue
+ }
+
+ val category = currentCategory ?: continue
+
+ changePattern.matchMatcher(line) {
+ val author = group("author")
+ 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 getCategory(categories: MutableList<Category>, newName: String): Category {
+ for (category in categories) {
+ if (category.name == newName) {
+ return category
+ }
+ }
+ val category = Category(newName)
+ categories.add(category)
+ return category
+}
+
+class Category(val name: String)
+
+class Change(val text: String, val category: Category, val prLink: String, val author: String) {
+ val extraInfo = mutableListOf<String>()
+}
diff --git a/src/main/kotlin/PullRequest.kt b/src/main/kotlin/PullRequest.kt
new file mode 100644
index 0000000..cd9122f
--- /dev/null
+++ b/src/main/kotlin/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<Any>, // Change type if you have specific assignees structure
+ @SerializedName("requested_reviewers") val requestedReviewers: List<Any>, // Change type if you have specific requested reviewers structure
+ @SerializedName("requested_teams") val requestedTeams: List<Any>, // Change type if you have specific requested teams structure
+ val labels: List<Any>, // 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