mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2024-12-21 10:42:48 +01:00
997 lines
32 KiB
Plaintext
997 lines
32 KiB
Plaintext
/*
|
|
* 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.0")
|
|
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.0.2")
|
|
classpath("io.ktor:ktor-client-cio:3.0.2")
|
|
classpath("io.ktor:ktor-client-auth:3.0.2")
|
|
classpath("io.ktor:ktor-client-content-negotiation:3.0.2")
|
|
classpath("io.ktor:ktor-serialization-kotlinx-json:3.0.2")
|
|
|
|
// This comes from the changelog plugin
|
|
// classpath("org.jetbrains:markdown:0.3.1")
|
|
}
|
|
}
|
|
|
|
plugins {
|
|
java
|
|
kotlin("jvm") version "2.0.0"
|
|
application
|
|
id("java-test-fixtures")
|
|
id("org.jetbrains.intellij.platform") version "2.2.0"
|
|
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.0-1.0.23"
|
|
}
|
|
|
|
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.1")
|
|
|
|
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()
|
|
instrumentationTools()
|
|
|
|
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")
|
|
plugin("com.intellij.classic.ui", "242.20224.159")
|
|
}
|
|
|
|
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.10.5")
|
|
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.5")
|
|
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.5")
|
|
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api:5.10.5")
|
|
testFixturesImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.5")
|
|
testFixturesImplementation("org.junit.jupiter:junit-jupiter-params:5.10.5")
|
|
|
|
// 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.10.5")
|
|
// testFixturesImplementation("org.junit.vintage:junit-vintage-engine:5.10.3")
|
|
}
|
|
|
|
configurations {
|
|
runtimeClasspath {
|
|
exclude(group = "org.antlr", module = "antlr4")
|
|
}
|
|
}
|
|
|
|
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 {
|
|
enabled = false
|
|
|
|
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 {
|
|
"""
|
|
|
|
|
|* [![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
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|