mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2024-11-25 16:42:55 +01:00
1011 lines
33 KiB
Plaintext
1011 lines
33 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.changelog.exceptions.MissingVersionException
|
|
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:1.8.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:6.7.0.202309050840-r")
|
|
classpath("org.kohsuke:github-api:1.305")
|
|
|
|
classpath("io.ktor:ktor-client-core:2.3.6")
|
|
classpath("io.ktor:ktor-client-cio:2.3.6")
|
|
classpath("io.ktor:ktor-client-auth:2.3.6")
|
|
classpath("io.ktor:ktor-client-content-negotiation:2.3.6")
|
|
classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.6")
|
|
|
|
// This comes from the changelog plugin
|
|
// classpath("org.jetbrains:markdown:0.3.1")
|
|
}
|
|
}
|
|
|
|
plugins {
|
|
antlr
|
|
java
|
|
kotlin("jvm") version "1.8.21"
|
|
application
|
|
|
|
id("org.jetbrains.intellij") version "1.16.0"
|
|
id("org.jetbrains.changelog") version "2.2.0"
|
|
|
|
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
|
|
// id("org.jlleitschuh.gradle.ktlint") version "11.3.1"
|
|
|
|
id("org.jetbrains.kotlinx.kover") version "0.6.1"
|
|
id("com.dorongold.task-tree") version "2.1.1"
|
|
|
|
id("com.google.devtools.ksp") version "1.8.21-1.0.11"
|
|
}
|
|
|
|
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("kspKotlin").configure { dependsOn("generateGrammarSource") }
|
|
tasks.named("kspTestKotlin").configure { enabled = false }
|
|
}
|
|
|
|
// Import variables from gradle.properties file
|
|
val javaVersion: String by project
|
|
val kotlinVersion: String by project
|
|
val ideaVersion: String by project
|
|
val downloadIdeaSources: String by project
|
|
val instrumentPluginCode: String by project
|
|
val remoteRobotVersion: String by project
|
|
val antlrVersion: String by project
|
|
|
|
val publishChannels: String by project
|
|
val publishToken: String by project
|
|
|
|
val slackUrl: String by project
|
|
val youtrackToken: String by project
|
|
|
|
repositories {
|
|
mavenCentral()
|
|
maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
|
|
}
|
|
|
|
dependencies {
|
|
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
|
|
compileOnly("org.jetbrains:annotations:24.1.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")
|
|
|
|
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-test
|
|
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
|
|
|
// https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
|
|
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
|
|
|
testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
|
|
testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
|
|
testImplementation("com.automation-remarks:video-recorder-junit:2.0")
|
|
runtimeOnly("org.antlr:antlr4-runtime:$antlrVersion")
|
|
antlr("org.antlr:antlr4:$antlrVersion")
|
|
|
|
api(project(":vim-engine"))
|
|
|
|
ksp(project(":annotation-processors"))
|
|
implementation(project(":annotation-processors"))
|
|
|
|
testApi("com.squareup.okhttp3:okhttp:4.12.0")
|
|
|
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
|
|
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.1")
|
|
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.1")
|
|
}
|
|
|
|
configurations {
|
|
runtimeClasspath {
|
|
exclude(group = "org.antlr", module = "antlr4")
|
|
}
|
|
}
|
|
|
|
// --- Compilation
|
|
// This can be moved to other test registration when issue with tests in gradle will be fixed
|
|
tasks.register<Test>("testWithNeovim") {
|
|
group = "verification"
|
|
systemProperty("ideavim.nvim.test", "true")
|
|
exclude("/ui/**")
|
|
exclude("**/longrunning/**")
|
|
exclude("**/propertybased/**")
|
|
useJUnitPlatform()
|
|
}
|
|
|
|
tasks.register<Test>("testPropertyBased") {
|
|
group = "verification"
|
|
// include("**/propertybased/**")
|
|
useJUnitPlatform()
|
|
}
|
|
|
|
tasks.register<Test>("testLongRunning") {
|
|
group = "verification"
|
|
// include("**/longrunning/**")
|
|
useJUnitPlatform()
|
|
}
|
|
|
|
tasks {
|
|
// Issue in gradle 7.3
|
|
val test by getting(Test::class) {
|
|
isScanForTestClasses = false
|
|
// Only run tests from classes that end with "Test"
|
|
include("**/*Test.class")
|
|
include("**/*test.class")
|
|
include("**/*Tests.class")
|
|
exclude("**/ParserTest.class")
|
|
}
|
|
|
|
val testWithNeovim by getting(Test::class) {
|
|
isScanForTestClasses = false
|
|
// Only run tests from classes that end with "Test"
|
|
include("**/*Test.class")
|
|
include("**/*test.class")
|
|
include("**/*Tests.class")
|
|
exclude("**/ParserTest.class")
|
|
exclude("**/longrunning/**")
|
|
exclude("**/propertybased/**")
|
|
}
|
|
|
|
val testPropertyBased by getting(Test::class) {
|
|
isScanForTestClasses = false
|
|
// Only run tests from classes that end with "Test"
|
|
include("**/propertybased/*Test.class")
|
|
include("**/propertybased/*test.class")
|
|
include("**/propertybased/*Tests.class")
|
|
}
|
|
|
|
val testLongRunning by getting(Test::class) {
|
|
isScanForTestClasses = false
|
|
// Only run tests from classes that end with "Test"
|
|
include("**/longrunning/**/*Test.class")
|
|
include("**/longrunning/**/*test.class")
|
|
include("**/longrunning/**/*Tests.class")
|
|
exclude("**/longrunning/**/ParserTest.class")
|
|
}
|
|
|
|
compileJava {
|
|
sourceCompatibility = javaVersion
|
|
targetCompatibility = javaVersion
|
|
|
|
options.encoding = "UTF-8"
|
|
}
|
|
|
|
compileKotlin {
|
|
kotlinOptions {
|
|
jvmTarget = javaVersion
|
|
apiVersion = "1.6"
|
|
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
|
// allWarningsAsErrors = true
|
|
}
|
|
}
|
|
compileTestKotlin {
|
|
kotlinOptions {
|
|
jvmTarget = javaVersion
|
|
apiVersion = "1.6"
|
|
// allWarningsAsErrors = true
|
|
}
|
|
}
|
|
}
|
|
|
|
java {
|
|
toolchain {
|
|
languageVersion.set(JavaLanguageVersion.of(javaVersion))
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
explicitApi()
|
|
jvmToolchain {
|
|
languageVersion.set(JavaLanguageVersion.of(javaVersion))
|
|
}
|
|
}
|
|
|
|
gradle.projectsEvaluated {
|
|
tasks.compileJava {
|
|
// options.compilerArgs.add("-Werror")
|
|
options.compilerArgs.add("-Xlint:deprecation")
|
|
}
|
|
}
|
|
|
|
// --- Intellij plugin
|
|
|
|
intellij {
|
|
version.set(ideaVersion)
|
|
pluginName.set("IdeaVim")
|
|
|
|
updateSinceUntilBuild.set(false)
|
|
|
|
downloadSources.set(downloadIdeaSources.toBoolean())
|
|
instrumentCode.set(instrumentPluginCode.toBoolean())
|
|
intellijRepository.set("https://www.jetbrains.com/intellij-repository")
|
|
// Yaml is only used for testing. It's part of the IdeaIC distribution, but needs to be included as a reference
|
|
plugins.set(listOf("java", "AceJump:3.8.11", "yaml"/*, "Pythonid:231.8109.2", "com.intellij.clion-swift:231.8109.4"*/))
|
|
}
|
|
|
|
tasks {
|
|
downloadRobotServerPlugin {
|
|
version.set(remoteRobotVersion)
|
|
}
|
|
|
|
publishPlugin {
|
|
channels.set(publishChannels.split(","))
|
|
token.set(publishToken)
|
|
}
|
|
|
|
signPlugin {
|
|
certificateChain.set(providers.environmentVariable("CERTIFICATE_CHAIN"))
|
|
privateKey.set(providers.environmentVariable("PRIVATE_KEY"))
|
|
password.set(providers.environmentVariable("PRIVATE_KEY_PASSWORD"))
|
|
}
|
|
|
|
runIdeForUiTests {
|
|
systemProperty("robot-server.port", "8082")
|
|
systemProperty("ide.mac.message.dialogs.as.sheets", "false")
|
|
systemProperty("jb.privacy.policy.text", "<!--999.999-->")
|
|
systemProperty("jb.consents.confirmation.enabled", "false")
|
|
}
|
|
|
|
runPluginVerifier {
|
|
downloadDir.set("${project.buildDir}/pluginVerifier/ides")
|
|
teamCityOutputFormat.set(true)
|
|
// ideVersions.set(listOf("IC-2021.3.4"))
|
|
}
|
|
|
|
generateGrammarSource {
|
|
maxHeapSize = "128m"
|
|
arguments.addAll(listOf("-package", "com.maddyhome.idea.vim.vimscript.parser.generated", "-visitor"))
|
|
outputDirectory = file("src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated")
|
|
}
|
|
|
|
named("compileKotlin") {
|
|
dependsOn("generateGrammarSource")
|
|
}
|
|
named("compileTestKotlin") {
|
|
dependsOn("generateTestGrammarSource")
|
|
}
|
|
|
|
// Add plugin open API sources to the plugin ZIP
|
|
val createOpenApiSourceJar by registering(Jar::class) {
|
|
dependsOn("generateGrammarSource")
|
|
// Java sources
|
|
from(sourceSets.main.get().java) {
|
|
include("**/com/maddyhome/idea/vim/**/*.java")
|
|
}
|
|
from(project(":vim-engine").sourceSets.main.get().java) {
|
|
include("**/com/maddyhome/idea/vim/**/*.java")
|
|
}
|
|
// Kotlin sources
|
|
from(kotlin.sourceSets.main.get().kotlin) {
|
|
include("**/com/maddyhome/idea/vim/**/*.kt")
|
|
}
|
|
from(project(":vim-engine").kotlin.sourceSets.main.get().kotlin) {
|
|
include("**/com/maddyhome/idea/vim/**/*.kt")
|
|
}
|
|
destinationDirectory.set(layout.buildDirectory.dir("libs"))
|
|
archiveClassifier.set("src")
|
|
}
|
|
|
|
buildPlugin {
|
|
dependsOn(createOpenApiSourceJar)
|
|
from(createOpenApiSourceJar) { into("lib/src") }
|
|
}
|
|
|
|
val pluginVersion = version
|
|
// Don't forget to update plugin.xml
|
|
patchPluginXml {
|
|
// Get the latest available change notes from the changelog file
|
|
changeNotes.set(
|
|
provider {
|
|
with(changelog) {
|
|
val log = try {
|
|
getUnreleased()
|
|
} catch (e: MissingVersionException) {
|
|
getOrNull(pluginVersion.toString()) ?: getLatest()
|
|
}
|
|
renderItem(
|
|
log,
|
|
org.jetbrains.changelog.Changelog.OutputType.HTML,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// --- Linting
|
|
|
|
//ktlint {
|
|
// version.set("0.48.2")
|
|
//}
|
|
|
|
// --- Tests
|
|
|
|
tasks {
|
|
test {
|
|
useJUnitPlatform()
|
|
exclude("**/propertybased/**")
|
|
exclude("**/longrunning/**")
|
|
exclude("/ui/**")
|
|
}
|
|
}
|
|
|
|
tasks.register<Test>("testUi") {
|
|
group = "verification"
|
|
useJUnitPlatform()
|
|
include("/ui/**")
|
|
}
|
|
|
|
// --- 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()
|
|
}
|
|
|
|
kover {
|
|
instrumentation {
|
|
// set of test tasks names to exclude from instrumentation. The results of their execution will not be presented in the report
|
|
excludeTasks += "testPropertyBased"
|
|
excludeTasks += "testLongRunning"
|
|
excludeTasks += "testWithNeovim"
|
|
excludeTasks += "testUi"
|
|
}
|
|
}
|
|
|
|
// --- Slack notification
|
|
|
|
tasks.register("slackNotification") {
|
|
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 {
|
|
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 {
|
|
val tickets = getYoutrackTicketsByQuery("%23%7BReady+To+Release%7D")
|
|
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.toLowerCase()
|
|
val regex = "^fix\\((vim-\\d+)\\):".toRegex()
|
|
val findResult = regex.find(lowercaseMessage)
|
|
if (findResult != null) {
|
|
println("Message matches")
|
|
val value = findResult.groups[1]!!.value.toUpperCase()
|
|
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
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|