| /* | |
| * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. | |
| * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. | |
| */ | |
| import java.util.concurrent.TimeUnit | |
| import kotlin.system.exitProcess | |
| fun getCommits(fromRevision: String, toRevision: String): List<Commit> { | |
| val cmd = "git rev-list --cherry-pick --format=medium $fromRevision...$toRevision -- ." | |
| val process = ProcessBuilder(*(cmd.split(" ")).toTypedArray()) | |
| .redirectOutput(ProcessBuilder.Redirect.PIPE) | |
| .redirectError(ProcessBuilder.Redirect.INHERIT) | |
| .start() | |
| process.waitFor(60, TimeUnit.MINUTES) | |
| val commits = process.inputStream.bufferedReader().readText().split("\n\ncommit ") | |
| return commits.mapNotNull { commit -> | |
| val sanitizedCommit = commit.removePrefix("commit ").padEnd(2, '\n') | |
| val commitGroups = parseCommit.find(sanitizedCommit)?.groupValues ?: return@mapNotNull null | |
| if (commitGroups.size != 4) { | |
| // group 1: commit hash | |
| // group 2: commit message | |
| // group 3: title | |
| return@mapNotNull null | |
| } | |
| val commitId = commitGroups[1] | |
| val title = commitGroups[3] | |
| val commitMessage = commitGroups[2] | |
| val changeId = matchChangeId.find(commitMessage)?.groupValues?.let { it[1] } | |
| val relnote = matchRelnote.find(commitMessage)?.groupValues?.let { it[1] } | |
| val issues = matchIssue.findAll(commitMessage).mapNotNull { it.groups[1]?.value }.toList() | |
| return@mapNotNull Commit(commitId, title, changeId, relnote?.trim('\"'), issues) | |
| } | |
| } | |
| val parseCommit = | |
| Regex("^([0-9a-f]+)\\nAuthor:.*?\\nDate:.*?\\n[\\s]+((.*?)\\n[\\s\\S]+)", RegexOption.MULTILINE) | |
| val matchRelnote = | |
| Regex( | |
| "^\\s*Relnote:\\s+(\"{3}.+\"{3}|\".+\"|[^\\n]+)$", | |
| setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL), | |
| ) | |
| val matchChangeId = Regex("Change-Id:\\s+(I[0-9a-f]+)", RegexOption.IGNORE_CASE) | |
| val matchIssue = Regex("(?:Bug|Fixes):\\s+\\[?(\\d+|KT-\\d+)\\]?", RegexOption.IGNORE_CASE) | |
| data class Commit( | |
| val commit: String, | |
| val title: String, | |
| val changeId: String?, | |
| val relnote: String?, | |
| val issues: List<String>, | |
| ) | |
| fun commitToGitHubUrl(commit: String) = "https://github.com/JetBrains/kotlin/commit/$commit" | |
| fun issueToBuganizerUrl(issue: String): String = "https://issuetracker.google.com/issues/$issue" | |
| fun issueToYoutrackUrl(issue: String): String = "https://youtrack.jetbrains.com/issue/$issue" | |
| fun Commit.asReleaseNote(): String { | |
| val commitLink = "[`${commit.take(7)}`](${commitToGitHubUrl(commit)})" | |
| val issueLinks = issues.joinToString(", ") { issue -> | |
| if (issue.startsWith("KT-")) { | |
| "[`$issue`](${issueToYoutrackUrl(issue)})" | |
| } else { | |
| "[`b/$issue`](${issueToBuganizerUrl(issue)})" | |
| } | |
| } | |
| val link = if (issueLinks.isEmpty()) commitLink else issueLinks | |
| val text = (relnote ?: title).split(" ").joinToString(" ") { if (it.startsWith('@')) "`$it`" else it } | |
| return "- $link $text" | |
| } | |
| fun main(vararg args: String) { | |
| if (args.isEmpty()) { | |
| println( | |
| """ | |
| Usage: ./gradlew composeReleaseNotes --args="<from-tag> <to-tag>:" | |
| For example, to generate release notes for v2.0.0-RC2: | |
| ./gradlew composeReleaseNotes --args="v2.0.0-RC1 v2.0.0-RC2" | |
| """.trimIndent() | |
| ) | |
| exitProcess(1) | |
| } | |
| val ignoreRelnotes = listOf("n/a") | |
| val fromRevision = args[0] | |
| val toRevision = args[1] | |
| val [fixes, features] = getCommits(fromRevision, toRevision) | |
| .filterDuplicates() | |
| .filter { | |
| (it.relnote != null && !ignoreRelnotes.contains(it.relnote.lowercase())) || | |
| it.issues.isNotEmpty() | |
| } | |
| .partition { it.issues.isNotEmpty() } | |
| println("### Compose compiler") | |
| println("#### New features") | |
| features.forEach { println(it.asReleaseNote()) } | |
| println("#### Fixes") | |
| fixes.forEach { println(it.asReleaseNote()) } | |
| } | |
| // Some commits may be duplicated, e.g. if a commit was cherry-picked from another branch with changes to tests. | |
| fun List<Commit>.filterDuplicates(): List<Commit> { | |
| val counts = mutableMapOf<String, Int>() | |
| forEach { | |
| counts[it.title] = counts.getOrDefault(it.title, 0) + 1 | |
| } | |
| return filter { counts[it.title] == 1 } | |
| } |