diff --git a/create-changelog.kts b/create-changelog.kts new file mode 100755 index 000000000..785a86735 --- /dev/null +++ b/create-changelog.kts @@ -0,0 +1,219 @@ +#!/usr/bin/env kscript + +@file:DependsOn("org.json:json:20180813") +@file:DependsOn("org.apache.maven:maven-model:3.6.1") +@file:DependsOn("org.eclipse.jgit:org.eclipse.jgit:5.4.0.201906121030-r") +@file:DependsOn("org.slf4j:slf4j-simple:1.7.25") + +import org.apache.maven.model.io.xpp3.MavenXpp3Reader +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.json.JSONArray +import org.json.JSONObject +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import java.net.HttpURLConnection +import java.net.URL + +data class Change( + val sha: String, + val author: String, + val message: String, + val date: String) { + constructor(commit: JSONObject) : this( + sha = commit.getString("sha"), + author = (commit["author"] as JSONObject).getString("login"), + message = (commit["commit"] as JSONObject).getString("message"), + date = (((commit["commit"] as JSONObject)["author"] as JSONObject)).getString("date")) +} + +fun versionFromString(str: String): Version { + return if (str.contains("-")) { + val split = str.split("-") + val Mmp = split[0].split(".").map { it.toInt() }.toIntArray() + Version(Mmp[0], Mmp[1], Mmp[2], split[1]) + } else { + val Mmp = str.split(".").map { it.toInt() }.toIntArray() + Version(Mmp[0], Mmp[1], Mmp[2], null) + } +} + +data class Version( + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: String? = null) : Comparable { + + val versionString = preRelease?.let { "$major.$minor.$patch-$preRelease" } ?: "$major.$minor.$patch" + + override fun toString() = versionString + + override fun compareTo(other: Version): Int { + val majorComp = major.compareTo(other.major) + if (majorComp != 0) + return majorComp + val minorComp = minor.compareTo(other.minor) + if (minorComp != 0) + return minorComp + val patchComp = patch.compareTo(other.patch) + if (patchComp != 0) + return patchComp + if (preRelease === null && other.preRelease != null) + return 1 + if (other.preRelease === null && preRelease != null) + return -1 + else if (other.preRelease === null && preRelease === null) + return 0 + return preRelease!!.compareTo(other.preRelease!!) + } + + fun bump(major: Boolean = false, minor: Boolean = false, patch: Boolean = false): Version { + if (major) return Version(this.major + 1, 0, 0, null) + if (minor) return Version(this.major, this.minor + 1, 0, null) + if (patch) return Version(this.major, this.minor, this.patch + 1, null) + return Version(this.major, this.minor, this.patch, this.preRelease) + } + +} + +val GITHUB_API_URL = "https://api.github.com"; + +fun getTags(repo: String) = JSONArray(fromURL(URL("$GITHUB_API_URL/repos/$repo/tags"))).filterIsInstance() + +fun getCommitTags(repo: String) = getTags(repo).filter { it.has("commit") } + +fun getParentsFromTag(repo: String, tag: JSONObject) = JSONObject(fromURL(URL("$GITHUB_API_URL/repos/$repo/commits/${(tag["commit"] as JSONObject)["sha"]}")))["parents"] as JSONArray + +fun getParentFromTag(repo: String, tag: JSONObject) = getParentsFromTag(repo, tag) + .also { require(it.length() == 1) { "Release tags must have exactly one parent but got $it" } }[0] as JSONObject + +fun compare(repo: String, shaFrom: String, shaTo: String): List { + val compareUrl = URL("$GITHUB_API_URL/repos/$repo/compare/$shaFrom...$shaTo") + val compareObj = JSONObject(fromURL(compareUrl)) + val commits = compareObj["commits"] as JSONArray + return commits.map { Change(it as JSONObject) } +} + + +fun fromURL(url: URL): String { + val conn = url.openConnection() as HttpURLConnection; + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + + if (conn.getResponseCode() != 200) + throw RuntimeException("Failed : HTTP error code : ${conn.responseCode}") + + val br = conn.inputStream.bufferedReader(); + val text = br.use(BufferedReader::readText) + conn.disconnect() + return text +} + +fun generateChangelog() { + + val reader = MavenXpp3Reader() + val model = FileReader("pom.xml").use { reader.read(it) } + val isSnapshot = model.version.contains("-SNAPSHOT") + val version = versionFromString(model.version) + + val tags = getCommitTags("saalfeldlab/paintera").filter { + try { + versionFromString(it.getString("name").replace("paintera-", "")) + true + } catch (e: Exception) { + false + } + } + + val repo = FileRepositoryBuilder().setGitDir(File(".git")).build() + val head = repo.resolve("HEAD") + val commitTo = if (isSnapshot) { + head.name + } else { + "paintera-${model.version}" + .takeUnless { t -> tags.count { it.getString("name") == t } == 0 } + ?: head.name + } + + val tagFrom = tags.first { versionFromString(it.getString("name").replace("paintera-", "")) < version } + val commitFrom = getParentFromTag("saalfeldlab/paintera", tagFrom) + val relevantCommits = compare( + repo = "saalfeldlab/paintera", + shaFrom = commitFrom.getString("sha"), + shaTo = commitTo) + val mergeCommits = relevantCommits.filter { it.message.startsWith("Merge pull request #") } + + val breaking = mutableListOf() + val features = mutableListOf() + val fixes = mutableListOf() + val unversioned = mutableListOf() + val visitedCommits = mutableSetOf() + val pullRequests = mutableListOf>() + + val regex = "Merge pull request #([0-9]+)".toRegex() + + for (commit in mergeCommits) { + if (visitedCommits.contains(commit.sha)) continue + visitedCommits.add(commit.sha) + val lines = commit.message.lines() + val matchResult = regex.find(lines[0]) + val pullRequestNumber = if (matchResult === null) { + -1 + } else { + matchResult.groupValues[1].toInt() + } + pullRequests += Pair(pullRequestNumber, commit.message) + var isAnything = false + for (line in lines) { + var l = line + val isBreaking = if (l.contains("[BREAKING]")) { + l = l.replace("[BREAKING]", "").trim() + true + } else false + val isFeature = if (l.contains("[FEATURE]")) { + l = l.replace("[FEATURE]", "").trim() + true + } else false + val isBugfix = if (l.contains("[BUGFIX]")) { + l = l.replace("[BUGFIX]", "").trim() + true + } else false + val isUnversioned = if (l.contains("[UNVERSIONED]")) { + l = l.replace("[UNVERSIONED]", "").trim() + true + } else false + isAnything = isBreaking || isFeature || isBugfix || isUnversioned + if (pullRequestNumber > 0) l = "$l (#$pullRequestNumber)" + if (isBreaking) breaking.add(l) + if (isFeature) features.add(l) + if (isBugfix) fixes.add(l) + if (isUnversioned) unversioned.add(l) + } + if (!isAnything) { + unversioned.add(commit.message.replace("\n+".toRegex(), ": ")) + } + } + + val versionFrom = versionFromString(tagFrom.getString("name").replace("paintera-", "")) + val suggestedVersion = versionFrom.bump(major = !breaking.isEmpty(), minor = !features.isEmpty(), patch = !fixes.isEmpty()) + + var text = "# Paintera $suggestedVersion\nPrevious release: $versionFrom\n\n\n## Changelog" + + if (!breaking.isEmpty()) + text = "$text\n\n### Breaking Changes${breaking.map { "\n - $it" }.joinToString("")}" + + if (!features.isEmpty()) + text = "$text\n\n### New Features${features.map { "\n - $it" }.joinToString("")}" + + if (!fixes.isEmpty()) + text = "$text\n\n### Bug Fixes${fixes.map { "\n - $it" }.joinToString("")}" + + if (!unversioned.isEmpty()) + text = "$text\n\n### Other${unversioned.map { "\n - $it" }.joinToString("")}" + val pullRequestStrings = pullRequests.map { "### #${it.first}\n${it.second}" } + text = "$text\n\n\n## Pull Requests\n\n${pullRequestStrings.joinToString("\n\n")}" + + println(text) +} + +generateChangelog()