/* * Copyright 2003-2023 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at * https://opensource.org/licenses/MIT. */ import dev.feedforward.markdownto.DownParser import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.RepositoryBuilder import org.intellij.markdown.ast.getTextInNode import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware import org.kohsuke.github.GHUser import java.net.HttpURLConnection import java.net.URL buildscript { repositories { mavenCentral() maven { url = uri("https://jitpack.io") } } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") classpath("com.github.AlexPl292:mark-down-to-slack:1.1.2") classpath("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r") // This is needed for jgit to connect to ssh classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.1.0.202411261347-r") classpath("org.kohsuke:github-api:1.305") classpath("io.ktor:ktor-client-core:3.1.1") classpath("io.ktor:ktor-client-cio:3.1.1") classpath("io.ktor:ktor-client-auth:3.1.1") classpath("io.ktor:ktor-client-content-negotiation:3.1.1") classpath("io.ktor:ktor-serialization-kotlinx-json:3.1.1") // This comes from the changelog plugin // classpath("org.jetbrains:markdown:0.3.1") } } plugins { java kotlin("jvm") version "2.0.21" application id("java-test-fixtures") id("org.jetbrains.intellij.platform") version "2.2.2-SNAPSHOT" id("org.jetbrains.changelog") version "2.2.1" id("org.jetbrains.kotlinx.kover") version "0.6.1" id("com.dorongold.task-tree") version "4.0.0" id("com.google.devtools.ksp") version "2.0.21-1.0.25" } val moduleSources by configurations.registering // Import variables from gradle.properties file val javaVersion: String by project val kotlinVersion: String by project val ideaVersion: String by project val ideaType: String by project val instrumentPluginCode: String by project val remoteRobotVersion: String by project val publishChannels: String by project val publishToken: String by project val slackUrl: String by project val youtrackToken: String by project val releaseType: String? by project repositories { mavenCentral() intellijPlatform { defaultRepositories() } } dependencies { api(project(":vim-engine")) ksp(project(":annotation-processors")) compileOnly(project(":annotation-processors")) compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") compileOnly("org.jetbrains:annotations:26.0.2") intellijPlatform { // Snapshots don't use installers // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html#target-versions-installers val useInstaller = "EAP-SNAPSHOT" !in ideaVersion // Note that it is also possible to use local("...") to compile against a locally installed IDE // E.g. local("/Users/{user}/Applications/IntelliJ IDEA Ultimate.app") // Or something like: intellijIdeaUltimate(ideaVersion) create(ideaType, ideaVersion, useInstaller) pluginVerifier() zipSigner() testFramework(TestFrameworkType.Platform) testFramework(TestFrameworkType.JUnit5) // AceJump is an optional dependency. We use their SessionManager class to check if it's active plugin("AceJump", "3.8.19") bundledPlugins("org.jetbrains.plugins.terminal", "com.intellij.modules.json") } moduleSources(project(":vim-engine", "sourcesJarArtifacts")) // --------- Test dependencies ---------- testApi("com.squareup.okhttp3:okhttp:4.12.0") // https://mvnrepository.com/artifact/com.ensarsarajcic.neovim.java/neovim-api testImplementation("com.ensarsarajcic.neovim.java:neovim-api:0.2.3") testImplementation("com.ensarsarajcic.neovim.java:core-rpc:0.2.3") testFixturesImplementation("com.ensarsarajcic.neovim.java:neovim-api:0.2.3") testFixturesImplementation("com.ensarsarajcic.neovim.java:core-rpc:0.2.3") // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-test testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") testFixturesImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") // https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.12.0") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.12.0") testImplementation("org.junit.jupiter:junit-jupiter-params:5.12.0") testFixturesImplementation("org.junit.jupiter:junit-jupiter-api:5.12.0") testFixturesImplementation("org.junit.jupiter:junit-jupiter-engine:5.12.0") testFixturesImplementation("org.junit.jupiter:junit-jupiter-params:5.12.0") // Temp workaround suggested in https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4 // Can be removed when IJPL-159134 is fixed // testRuntimeOnly("junit:junit:4.13.2") testImplementation("org.junit.vintage:junit-vintage-engine:5.12.0") // testFixturesImplementation("org.junit.vintage:junit-vintage-engine:5.10.3") } configurations { runtimeClasspath { exclude(group = "org.antlr", module = "antlr4") } } val currentJavaVersion = javaToolchains.launcherFor {}.get().metadata.languageVersion.toString() if (currentJavaVersion != javaVersion) { // NOTE: I made this exception because the default Gradle error message is horrible, noone can understand it. throw RuntimeException( """ Incorrect java version used for building. IdeaVim uses java version $javaVersion, but the current java version is $currentJavaVersion. If IntelliJ IDEA is used, change the setting in "Settings | Build, Execution, Deployment | Build Tools | Gradle" If build is run from the terminal, set JAVA_HOME environment variable to the correct java version. """.trimIndent() ) } tasks { test { useJUnitPlatform() // Set teamcity env variable locally to run additional tests for leaks. // By default, this test runs on TC only, but this test doesn't take a lot of time, // so we can turn it on for local development if (environment["TEAMCITY_VERSION"] == null) { println("Set env TEAMCITY_VERSION to X to enable project leak checks from the platform") environment("TEAMCITY_VERSION" to "X") } systemProperty("ideavim.nvim.test", System.getProperty("nvim") ?: false) } compileJava { // CodeQL can't resolve the 'by project' property, so we need to give it a hint. This is the minimum version we need // so doesn't have to match exactly // Hint for the CodeQL autobuilder: sourceCompatibility = 17 sourceCompatibility = javaVersion targetCompatibility = javaVersion options.encoding = "UTF-8" } compileKotlin { kotlinOptions { jvmTarget = javaVersion // See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library // For the list of bundled versions apiVersion = "1.9" freeCompilerArgs = listOf( "-Xjvm-default=all-compatibility", // Needed to compile the AceJump which uses kotlin beta // Without these two option compilation fails "-Xskip-prerelease-check", "-Xallow-unstable-dependencies", ) // allWarningsAsErrors = true } } compileTestKotlin { kotlinOptions { jvmTarget = javaVersion apiVersion = "1.9" // Needed to compile the AceJump which uses kotlin beta // Without these two option compilation fails freeCompilerArgs += listOf("-Xskip-prerelease-check", "-Xallow-unstable-dependencies") // allWarningsAsErrors = true } } // Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use // a custom task (see below) runIde { systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true) } // Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies // Note that the version must be greater than the plugin's target version, for obvious reasons // You can also set splitMode and splitModeTarget here to test split mode in a custom IDE // val runIdeCustom by intellijPlatformTesting.runIde.registering { // type = IntelliJPlatformType.Rider // version = "2024.1.2" // } // Uncomment to run the plugin in a locally installed IDE // val runIdeLocal by intellijPlatformTesting.runIde.registering { // localPath = file("/Users/{user}/Applications/WebStorm.app") // } val runIdeForUiTests by intellijPlatformTesting.runIde.registering { task { jvmArgumentProviders += CommandLineArgumentProvider { listOf( "-Drobot-server.port=8082", "-Dide.mac.message.dialogs.as.sheets=false", "-Djb.privacy.policy.text=<!--999.999-->", "-Djb.consents.confirmation.enabled=false", "-Dide.show.tips.on.startup.default.value=false", "-Doctopus.handler=" + (System.getProperty("octopus.handler") ?: true), ) } } plugins { robotServerPlugin(remoteRobotVersion) } } val runIdeSplitMode by intellijPlatformTesting.runIde.registering { splitMode = true splitModeTarget = SplitModeAware.SplitModeTarget.FRONTEND } // Add plugin open API sources to the plugin ZIP val sourcesJar by registering(Jar::class) { dependsOn(moduleSources) destinationDirectory.set(layout.buildDirectory.dir("libs")) archiveClassifier.set(DocsType.SOURCES) from(sourceSets.main.map { it.kotlin }) from(provider { moduleSources.map { it.map { jarFile -> zipTree(jarFile) } } }) } buildPlugin { dependsOn(sourcesJar) from(sourcesJar) { into("lib/src") } } } java { toolchain { languageVersion.set(JavaLanguageVersion.of(javaVersion)) } } kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(javaVersion)) } } gradle.projectsEvaluated { tasks.compileJava { // options.compilerArgs.add("-Werror") options.compilerArgs.add("-Xlint:deprecation") } } // --- Intellij plugin intellijPlatform { pluginConfiguration { name = "IdeaVim" changeNotes.set( """ Undo in IdeaVim now works like in Vim<br/> Caret movement is no longer a separate undo step, and full insert is undoable in one step.<br/> <a href="https://youtrack.jetbrains.com/issue/VIM-547/Undo-splits-Insert-mode-edits-into-separate-undo-chunks">Share Feedback</a> <br/> <br/> <a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a> """.trimIndent() ) ideaVersion { // Let the Gradle plugin set the since-build version. It defaults to the version of the IDE we're building against // specified as two components, `{branch}.{build}` (e.g., "241.15989"). There is no third component specified. // The until-build version defaults to `{branch}.*`, but we want to support _all_ future versions, so we set it // with a null provider (the provider is important). // By letting the Gradle plugin handle this, the Plugin DevKit IntelliJ plugin cannot help us with the "Usage of // IntelliJ API not available in older IDEs" inspection. However, since our since-build is the version we compile // against, we can never get an API that's newer - it would be an unresolved symbol. untilBuild.set(provider { null }) } } publishing { channels.set(publishChannels.split(",")) token.set(publishToken) } signing { certificateChain.set(providers.environmentVariable("CERTIFICATE_CHAIN")) privateKey.set(providers.environmentVariable("PRIVATE_KEY")) password.set(providers.environmentVariable("PRIVATE_KEY_PASSWORD")) } verifyPlugin { teamCityOutputFormat = true ides { recommended() } } instrumentCode.set(instrumentPluginCode.toBoolean()) } ksp { arg("generated_directory", "$projectDir/src/main/resources/ksp-generated") arg("vimscript_functions_file", "intellij_vimscript_functions.json") arg("ex_commands_file", "intellij_ex_commands.json") arg("commands_file", "intellij_commands.json") } afterEvaluate { // tasks.named("kspKotlin").configure { dependsOn("clean") } tasks.named("kspTestFixturesKotlin").configure { enabled = false } tasks.named("kspTestFixturesKotlin").configure { enabled = false } tasks.named("kspTestKotlin").configure { enabled = false } } // --- Changelog changelog { groups.set(listOf("Features:", "Changes:", "Deprecations:", "Fixes:", "Merged PRs:")) itemPrefix.set("*") path.set("${project.projectDir}/CHANGES.md") unreleasedTerm.set("To Be Released") headerParserRegex.set("(\\d\\.\\d+(.\\d+)?)".toRegex()) // header = { "${project.version}" } // version = "0.60" } // --- Kover koverMerged { enable() } // --- Slack notification tasks.register("slackNotification") { doLast { if (version.toString().last() != '0') return@doLast if (slackUrl.isBlank()) { println("Slack Url is not defined") return@doLast } val changeLog = changelog.renderItem(changelog.getLatest(), Changelog.OutputType.PLAIN_TEXT) val slackDown = DownParser(changeLog, true).toSlack().toString() //language=JSON val message = """ { "text": "New version of IdeaVim", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "IdeaVim $version has been released\n$slackDown" } } ] } """.trimIndent() println("Parsed data: $slackDown") val post = URL(slackUrl) with(post.openConnection() as HttpURLConnection) { requestMethod = "POST" doOutput = true setRequestProperty("Content-Type", "application/json") outputStream.write(message.toByteArray()) val postRc = responseCode println("Response code: $postRc") if (postRc == 200) { println(inputStream.bufferedReader().use { it.readText() }) } else { println(errorStream.bufferedReader().use { it.readText() }) } } } } // Uncomment to enable FUS testing mode // tasks { // withType<org.jetbrains.intellij.tasks.RunIdeTask> { // jvmArgs("-Didea.is.internal=true") // jvmArgs("-Dfus.internal.test.mode=true") // } // } // --- Update authors tasks.register("updateAuthors") { doLast { val uncheckedEmails = setOf( "aleksei.plate@jetbrains.com", "aleksei.plate@teamcity", "aleksei.plate@TeamCity", "alex.plate@192.168.0.109", "nikita.koshcheev@TeamCity", "TeamCity@TeamCity", ) updateAuthors(uncheckedEmails) } } val prId: String by project tasks.register("updateMergedPr") { doLast { val x = changelog.getUnreleased() println("x") // if (project.hasProperty("prId")) { // println("Got pr id: $prId") // updateMergedPr(prId.toInt()) // } else { // error("Cannot get prId") // } } } tasks.register("updateChangelog") { doLast { updateChangelog() } } tasks.register("updateYoutrackOnCommit") { doLast { updateYoutrackOnCommit() } } val vimProjectId = "22-43" val fixVersionsFieldId = "123-285" val fixVersionsFieldType = "VersionProjectCustomField" val fixVersionsElementType = "VersionBundleElement" tasks.register("releaseActions") { group = "other" doLast { if (releaseType == "patch") return@doLast val tickets = getYoutrackTicketsByQuery("%23%7BReady+To+Release%7D%20and%20tag:%20%7BIdeaVim%20Released%20In%20EAP%7D%20") if (tickets.isNotEmpty()) { println("Updating statuses for tickets: $tickets") setYoutrackStatus(tickets, "Fixed") println("Checking if version $version exists...") val versionId = getVersionIdByName(version.toString()) if (versionId == null) { addReleaseToYoutrack(version.toString()) } else { println("Version $version already exists in YouTrack. Version id: $versionId") } setYoutrackFixVersion(tickets, version.toString()) } else { println("No tickets to update statuses") } } } tasks.register("integrationsTest") { group = "other" doLast { val testTicketId = "VIM-2784" // YouTrack set to Ready To Release on Fix commit setYoutrackStatus(listOf(testTicketId), "Ready To Release") if ("Ready To Release" != getYoutrackStatus(testTicketId)) { error("Ticket status was not updated") } setYoutrackStatus(listOf(testTicketId), "Open") // Check YouTrack requests val prevStatus = getYoutrackStatus(testTicketId) setYoutrackStatus(listOf(testTicketId), "Ready To Release") val tickets = getYoutrackTicketsByQuery("%23%7BReady+To+Release%7D") if (testTicketId !in tickets) { error("Test ticket is not found in request") } setYoutrackStatus(listOf(testTicketId), prevStatus) // Check adding and removing release val existingVersionId = getVersionIdByName("TEST_VERSION") if (existingVersionId != null) { deleteVersionById(existingVersionId) } val versionId = addReleaseToYoutrack("TEST_VERSION") guard(getVersionIdByName("TEST_VERSION") != null) { "Test version isn't created" } setYoutrackStatus(listOf(testTicketId), "Fixed") setYoutrackFixVersion(listOf(testTicketId), "TEST_VERSION") deleteVersionById(versionId) setYoutrackStatus(listOf(testTicketId), "Open") guard(getVersionIdByName("TEST_VERSION") == null) { "Test version isn't deleted" } updateMergedPr(525) // TODO: test Ticket parsing // TODO: test Update CHANGES // TODO: test Update AUTHORS // TODO: test Slack notification // TODO: Add a comment on EAP release } } fun guard(check: Boolean, ifWrong: () -> String) { if (!check) { error(ifWrong()) } } tasks.register("testUpdateChangelog") { group = "verification" description = "This is a task to manually assert the correctness of the update tasks" doLast { val changesFile = File("$projectDir/CHANGES.md") val changes = changesFile.readText() val changesBuilder = StringBuilder(changes) val insertOffset = setupSection(changes, changesBuilder, "### Changes:") changesBuilder.insert(insertOffset, "--Hello--\n") changesFile.writeText(changesBuilder.toString()) } } fun addReleaseToYoutrack(name: String): String { val client = httpClient() println("Creating new release version in YouTrack: $name") return runBlocking { val response = client.post("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values?fields=id,name") { contentType(ContentType.Application.Json) accept(ContentType.Application.Json) val request = buildJsonObject { put("name", name) put("\$type", fixVersionsElementType) } setBody(request) } response.body<JsonObject>().getValue("id").jsonPrimitive.content } } fun getVersionIdByName(name: String): String? { val client = httpClient() return runBlocking { val response = client.get("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values?fields=id,name&query=$name") response.body<JsonArray>().singleOrNull()?.jsonObject?.get("id")?.jsonPrimitive?.content } } fun deleteVersionById(id: String) { val client = httpClient() runBlocking { client.delete("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values/$id") } } fun updateYoutrackOnCommit() { println("Start updating youtrack") println(projectDir) val newFixes = changes() val newTickets = newFixes.map { it.id } println("Set new status for $newTickets") setYoutrackStatus(newTickets, "Ready To Release") } fun getYoutrackTicketsByQuery(query: String): Set<String> { val client = httpClient() return runBlocking { val response = client.get("https://youtrack.jetbrains.com/api/issues/?fields=idReadable&query=project:VIM+$query") response.body<JsonArray>().mapTo(HashSet()) { it.jsonObject.getValue("idReadable").jsonPrimitive.content } } } fun setYoutrackStatus(tickets: Collection<String>, status: String) { val client = httpClient() runBlocking { for (ticket in tickets) { println("Try to set $ticket to $status") val response = client.post("https://youtrack.jetbrains.com/api/issues/$ticket?fields=customFields(id,name,value(id,name))") { contentType(ContentType.Application.Json) accept(ContentType.Application.Json) val request = buildJsonObject { putJsonArray("customFields") { addJsonObject { put("name", "State") put("\$type", "SingleEnumIssueCustomField") putJsonObject("value") { put("name", status) } } } } setBody(request) } println(response) println(response.body<String>()) if (!response.status.isSuccess()) { error("Request failed. $ticket, ${response.body<String>()}") } val finalState = response.body<JsonObject>()["customFields"]!!.jsonArray .single { it.jsonObject["name"]!!.jsonPrimitive.content == "State" } .jsonObject["value"]!! .jsonObject["name"]!! .jsonPrimitive.content if (finalState != status) { error("Ticket $ticket is not updated! Expected status $status, but actually $finalState") } } } } fun setYoutrackFixVersion(tickets: Collection<String>, version: String) { val client = httpClient() runBlocking { for (ticket in tickets) { println("Try to set fix version $version for $ticket") val response = client.post("https://youtrack.jetbrains.com/api/issues/$ticket?fields=customFields(id,name,value(id,name))") { contentType(ContentType.Application.Json) accept(ContentType.Application.Json) val request = buildJsonObject { putJsonArray("customFields") { addJsonObject { put("name", "Fix versions") put("\$type", "MultiVersionIssueCustomField") putJsonArray("value") { addJsonObject { put("name", version) } } } } } setBody(request) } println(response) println(response.body<String>()) if (!response.status.isSuccess()) { error("Request failed. $ticket, ${response.body<String>()}") } val finalState = response.body<JsonObject>()["customFields"]!!.jsonArray .single { it.jsonObject["name"]!!.jsonPrimitive.content == "Fix versions" } .jsonObject["value"]!! .jsonArray[0] .jsonObject["name"]!! .jsonPrimitive.content if (finalState != version) { error("Ticket $ticket is not updated! Expected fix version $version, but actually $finalState") } } } } fun getYoutrackStatus(ticket: String): String { val client = httpClient() return runBlocking { val response = client.get("https://youtrack.jetbrains.com/api/issues/$ticket/customFields/123-129?fields=value(name)") response.body<JsonObject>()["value"]!!.jsonObject.getValue("name").jsonPrimitive.content } } fun updateChangelog() { println("Start update authors") println(projectDir) val newFixes = changes() // Update changes file val changesFile = File("$projectDir/CHANGES.md") val changes = changesFile.readText() val changesBuilder = StringBuilder(changes) val insertOffset = setupSection(changes, changesBuilder, "### Fixes:") if (insertOffset < 50) error("Incorrect offset: $insertOffset") val firstPartOfChanges = changes.take(insertOffset) val actualFixes = newFixes .filterNot { it.id in firstPartOfChanges } val newUpdates = actualFixes .joinToString("") { "* [${it.id}](https://youtrack.jetbrains.com/issue/${it.id}) ${it.text}\n" } changesBuilder.insert(insertOffset, newUpdates) if (actualFixes.isNotEmpty()) { changesFile.writeText(changesBuilder.toString()) } } fun updateAuthors(uncheckedEmails: Set<String>) { println("Start update authors") println(projectDir) val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build() val git = Git(repository) val lastSuccessfulCommit = System.getenv("SUCCESS_COMMIT")!! val hashesAndEmailes = git.log().call() .takeWhile { !it.id.name.equals(lastSuccessfulCommit, ignoreCase = true) } .associate { it.authorIdent.emailAddress to it.name } println("Last successful commit: $lastSuccessfulCommit") println("Amount of commits: ${hashesAndEmailes.size}") println("Emails: ${hashesAndEmailes.keys}") val gitHub = org.kohsuke.github.GitHub.connect() val ghRepository = gitHub.getRepository("JetBrains/ideavim") val users = mutableSetOf<Author>() println("Start emails processing") for ((email, hash) in hashesAndEmailes) { println("Processing '$email'...") if (email in uncheckedEmails) { println("Email '$email' is in unchecked emails. Skip it") continue } if ("dependabot[bot]@users.noreply.github.com" in email) { println("Email '$email' is from dependabot. Skip it") continue } if ("tcuser" in email) { println("Email '$email' is from teamcity. Skip it") continue } val user: GHUser? = ghRepository.getCommit(hash).author if (user == null) { println("Cant get the commit author. Email: $email. Commit: $hash") continue } val htmlUrl = user.htmlUrl.toString() val name = user.name ?: user.login users.add(Author(name, htmlUrl, email)) } println("Emails processed") val authorsFile = File("$projectDir/AUTHORS.md") val authors = authorsFile.readText() val parser = org.intellij.markdown.parser.MarkdownParser(org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor()) val tree = parser.buildMarkdownTreeFromString(authors) val contributorsSection = tree.children[24] val existingEmails = mutableSetOf<String>() for (child in contributorsSection.children) { if (child.children.size > 1) { existingEmails.add( child.children[1].children[0].children[2].children[2].getTextInNode(authors).toString(), ) } } val newAuthors = users.filterNot { it.mail in existingEmails } if (newAuthors.isEmpty()) return val authorNames = newAuthors.joinToString(", ") { it.name } println("::set-output name=authors::$authorNames") val insertionString = newAuthors.toMdString() val resultingString = StringBuffer(authors).insert(contributorsSection.endOffset, insertionString).toString() authorsFile.writeText(resultingString) } fun List<Author>.toMdString(): String { return this.joinToString(separator = "") { """ | |* [![icon][mail]](mailto:${it.mail}) | [![icon][github]](${it.url}) | | ${it.name} """.trimMargin() } } data class Author(val name: String, val url: String, val mail: String) data class Change(val id: String, val text: String) fun updateMergedPr(number: Int) { val token = System.getenv("GITHUB_OAUTH") println("Token size: ${token.length}") val gitHub = org.kohsuke.github.GitHubBuilder().withOAuthToken(token).build() println("Connecting to the repo...") val repository = gitHub.getRepository("JetBrains/ideavim") println("Getting pull requests...") val pullRequest = repository.getPullRequest(number) if (pullRequest.user.login == "dependabot[bot]") return val changesFile = File("$projectDir/CHANGES.md") val changes = changesFile.readText() val changesBuilder = StringBuilder(changes) val insertOffset = setupSection(changes, changesBuilder, "### Merged PRs:") if (insertOffset < 50) error("Incorrect offset: $insertOffset") if (pullRequest.user.login == "dependabot[bot]") return val prNumber = pullRequest.number val userName = pullRequest.user.name ?: pullRequest.user.login val login = pullRequest.user.login val title = pullRequest.title val section = "* [$prNumber](https://github.com/JetBrains/ideavim/pull/$prNumber) by [$userName](https://github.com/$login): $title\n" changesBuilder.insert(insertOffset, section) changesFile.writeText(changesBuilder.toString()) } fun setupSection( changes: String, authorsBuilder: StringBuilder, sectionName: String, ): Int { val parser = org.intellij.markdown.parser.MarkdownParser(org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor()) val tree = parser.buildMarkdownTreeFromString(changes) var idx = -1 for (index in tree.children.indices) { if (tree.children[index].getTextInNode(changes).startsWith("## ")) { idx = index break } } val hasToBeReleased = tree.children[idx].getTextInNode(changes).contains("To Be Released") return if (hasToBeReleased) { var mrgIdx = -1 for (index in (idx + 1) until tree.children.lastIndex) { val textInNode = tree.children[index].getTextInNode(changes) val foundIndex = textInNode.startsWith(sectionName) if (foundIndex) { var filledPr = index + 2 while (tree.children[filledPr].getTextInNode(changes).startsWith("*")) { filledPr++ } mrgIdx = tree.children[filledPr].startOffset + 1 break } else { val currentSectionIndex = sections.indexOf(sectionName) val insertHere = textInNode.startsWith("## ") || textInNode.startsWith("### ") && sections.indexOfFirst { textInNode.startsWith(it) } .let { if (it < 0) false else it > currentSectionIndex } if (insertHere) { val section = """ $sectionName """.trimIndent() authorsBuilder.insert(tree.children[index].startOffset, section) mrgIdx = tree.children[index].startOffset + (section.length - 1) break } } } mrgIdx } else { val section = """ ## To Be Released $sectionName """.trimIndent() authorsBuilder.insert(tree.children[idx].startOffset, section) tree.children[idx].startOffset + (section.length - 1) } } val sections = listOf( "### Features:", "### Changes:", "### Fixes:", "### Merged PRs:", ) fun changes(): List<Change> { val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build() val git = Git(repository) val lastSuccessfulCommit = System.getenv("SUCCESS_COMMIT")!! val messages = git.log().call() .takeWhile { !it.id.name.equals(lastSuccessfulCommit, ignoreCase = true) } .map { it.shortMessage } // Collect fixes val newFixes = mutableListOf<Change>() println("Last successful commit: $lastSuccessfulCommit") println("Amount of commits: ${messages.size}") println("Start changes processing") for (message in messages) { println("Processing '$message'...") val lowercaseMessage = message.lowercase() val regex = "^fix\\((vim-\\d+)\\):".toRegex() val findResult = regex.find(lowercaseMessage) if (findResult != null) { println("Message matches") val value = findResult.groups[1]!!.value.uppercase() val shortMessage = message.drop(findResult.range.last + 1).trim() newFixes += Change(value, shortMessage) } else { println("Message doesn't match") } } return newFixes } fun httpClient(): HttpClient { return HttpClient(CIO) { expectSuccess = true install(Auth) { bearer { loadTokens { val accessToken = youtrackToken.ifBlank { System.getenv("YOUTRACK_TOKEN")!! } BearerTokens(accessToken, "") } } } install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true }, ) } } }