mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[JEWEL-1027] Fix the branch comparison script
The script had issues in picking up which commits to look for and did not account for things like the time difference between a commit happening on master and its cherry picks happening on a branch. This fixes the issue and cleans up the script code, improving logging too. It also adds a few flags to make the output of the script a bit more manageable. closes https://github.com/JetBrains/intellij-community/pull/3252 (cherry picked from commit e8318e2cddbd84c22b94d93df4a2985ef04c8de8) (cherry picked from commit 5504a8b1f647a318a32f78787054e29e22f1f0b2) IJ-MR-179919 GitOrigin-RevId: fcdd3eb004887792a39d30b6f55362c4d17070f4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
72035a552a
commit
d4feb74e73
@@ -20,6 +20,7 @@ import org.jetbrains.jps.model.module.JpsModuleDependency
|
||||
import kotlin.io.path.exists
|
||||
|
||||
private const val GROUP_ID: String = "org.jetbrains.jewel"
|
||||
|
||||
private val CORE: Map<String, String> = mapOf(
|
||||
"intellij.platform.jewel.foundation" to "jewel-foundation",
|
||||
"intellij.platform.jewel.markdown.core" to "jewel-markdown-core",
|
||||
@@ -30,7 +31,11 @@ private val CORE: Map<String, String> = mapOf(
|
||||
"intellij.platform.jewel.markdown.extensions.gfmAlerts" to "jewel-markdown-extensions-gfm-alerts",
|
||||
"intellij.platform.jewel.markdown.extensions.images" to "jewel-markdown-extensions-images",
|
||||
)
|
||||
private val NOT_PUBLISHED: Set<String> = setOf("intellij.platform.jewel.ideLafBridge", "intellij.platform.jewel.markdown.ideLafBridgeStyling")
|
||||
|
||||
private val NOT_PUBLISHED: Set<String> = setOf(
|
||||
"intellij.platform.jewel.ideLafBridge",
|
||||
"intellij.platform.jewel.markdown.ideLafBridgeStyling",
|
||||
)
|
||||
|
||||
private val transitiveJewelDependencies = mapOf(
|
||||
"jewel-foundation" to emptySet(),
|
||||
@@ -69,9 +74,8 @@ internal object JewelMavenArtifacts {
|
||||
DependenciesProperties(COMMUNITY_ROOT, jewelProperties).property("jewel.release.version")
|
||||
}
|
||||
|
||||
fun isPublishedJewelModule(module: JpsModule): Boolean {
|
||||
return module.name.startsWith("intellij.platform.jewel.") && module.name !in NOT_PUBLISHED
|
||||
}
|
||||
fun isPublishedJewelModule(module: JpsModule): Boolean =
|
||||
module.name.startsWith("intellij.platform.jewel.") && module.name !in NOT_PUBLISHED
|
||||
|
||||
fun patchCoordinates(module: JpsModule, coordinates: MavenCoordinates): MavenCoordinates {
|
||||
check(isPublishedJewelModule(module))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env kotlin
|
||||
@file:Suppress("RAW_RUN_BLOCKING")
|
||||
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
|
||||
@file:Import("utils.main.kts") // Assuming utils.main.kts is needed for runCommand and getLatestReleaseDate
|
||||
@file:Import("utils.main.kts")
|
||||
|
||||
import com.github.ajalt.clikt.command.SuspendingCliktCommand
|
||||
import com.github.ajalt.clikt.command.main
|
||||
@@ -10,28 +10,42 @@ import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.defaultLazy
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import kotlin.system.exitProcess
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/** Configuration for the branch comparison script. Defines paths to check and git log formatting options. */
|
||||
private object Config {
|
||||
/** Paths to monitor for changes - platform components and build files */
|
||||
val PATHS_TO_CHECK =
|
||||
listOf(
|
||||
"platform/jewel",
|
||||
"libraries/skiko",
|
||||
"libraries/compose-runtime-desktop",
|
||||
"libraries/compose-foundation-desktop",
|
||||
"libraries/compose-foundation-desktop-junit",
|
||||
"libraries/detekt-compose-rules",
|
||||
"libraries/compose-foundation-desktop",
|
||||
"build",
|
||||
"build/src/org/jetbrains/intellij/build/JewelMavenArtifacts.kt",
|
||||
"build/src/JewelMavenArtifactsBuildTarget.kt",
|
||||
)
|
||||
const val DELIMITER = "::COMMIT::"
|
||||
const val GIT_LOG_FORMAT = "%H${DELIMITER}%s"
|
||||
}
|
||||
|
||||
private data class CommitInfo(
|
||||
val hash: String,
|
||||
val subject: String,
|
||||
)
|
||||
/** Represents a git commit with its hash and subject line. */
|
||||
private data class CommitInfo(val hash: String, val subject: String)
|
||||
|
||||
/** Result of parsing a git log line */
|
||||
private sealed class ParseResult {
|
||||
data class Success(val commit: CommitInfo) : ParseResult()
|
||||
|
||||
object DateFiltered : ParseResult()
|
||||
|
||||
object ParseError : ParseResult()
|
||||
}
|
||||
|
||||
private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
private val verbose: Boolean by
|
||||
@@ -39,6 +53,21 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
private val jewelOnly: Boolean by
|
||||
option("--jewel-only", help = "Only prints commits whose subject contains the word 'jewel' (case insensitive).")
|
||||
.flag(default = false)
|
||||
private val showHashes: Boolean by
|
||||
option("--hash", help = "Show commit hashes in the output.").flag(default = false)
|
||||
private val noBuildChanges: Boolean by
|
||||
option(
|
||||
"--no-build-changes",
|
||||
help = "Exclude commits that only affect the build/ directory. Mutually exclusive with --build-only.",
|
||||
)
|
||||
.flag(default = false)
|
||||
private val buildOnly: Boolean by
|
||||
option(
|
||||
"--build-only",
|
||||
help =
|
||||
"Only check commits that affect the build/ directory. Mutually exclusive with --no-build-changes.",
|
||||
)
|
||||
.flag(default = false)
|
||||
private val branch1: String by argument(help = "The first branch to compare.")
|
||||
private val branch2: String by argument(help = "The second branch to compare.")
|
||||
private val sinceDate: String by
|
||||
@@ -63,10 +92,42 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
"Compares commits affecting specific paths between two branches by commit message."
|
||||
|
||||
override suspend fun run() {
|
||||
// Validate mutually exclusive flags
|
||||
if (noBuildChanges && buildOnly) {
|
||||
printlnErr("Error: --no-build-changes and --build-only are mutually exclusive.")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val pathsToCheck = validateEnvironment()
|
||||
val commits1 = fetchCommits(branch1, pathsToCheck)
|
||||
val commits2 = fetchCommits(branch2, pathsToCheck)
|
||||
|
||||
if (verbose) {
|
||||
println(
|
||||
"📊 Found ${commits1.size} commits in '${branch1}' and ${commits2.size} commits in '${branch2}' since ${sinceDate}"
|
||||
)
|
||||
|
||||
if (commits1.isNotEmpty()) {
|
||||
println("\n--- Commits for '${branch1}' ---")
|
||||
commits1.forEach(::printCommitInfoLine)
|
||||
println()
|
||||
}
|
||||
|
||||
if (commits2.isNotEmpty()) {
|
||||
println("\n--- Commits for '${branch2}' ---")
|
||||
commits2.forEach(::printCommitInfoLine)
|
||||
println()
|
||||
}
|
||||
}
|
||||
|
||||
compareAndDisplayResults(commits1, commits2)
|
||||
}
|
||||
|
||||
/** Validates the environment and prerequisites, returning the paths to check. */
|
||||
private suspend fun validateEnvironment(): List<String> {
|
||||
print("⏳ Locating IntelliJ Community root...")
|
||||
|
||||
val communityRoot = findCommunityRoot()
|
||||
|
||||
if (communityRoot == null || !communityRoot.isDirectory) {
|
||||
println()
|
||||
printlnErr(
|
||||
@@ -78,11 +139,13 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
|
||||
println(" DONE: ${communityRoot.absolutePath}")
|
||||
|
||||
// Validate git repository
|
||||
if (!isDirectoryGitRepo(communityRoot)) {
|
||||
printlnErr("Error: Not a git repository.")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// Validate branches exist
|
||||
if (!branchExists(branch1, communityRoot)) {
|
||||
printlnErr("Error: Branch '$branch1' not found.")
|
||||
exitProcess(1)
|
||||
@@ -93,62 +156,150 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// Ensure all paths to check exist
|
||||
// Determine which paths to check based on flags
|
||||
val pathsToCheck =
|
||||
when {
|
||||
noBuildChanges -> Config.PATHS_TO_CHECK.filterNot { it.startsWith("build") }
|
||||
buildOnly -> Config.PATHS_TO_CHECK.filter { it.startsWith("build") }
|
||||
else -> Config.PATHS_TO_CHECK
|
||||
}
|
||||
|
||||
// Validate paths exist
|
||||
var anyPathMissing = false
|
||||
for (path in Config.PATHS_TO_CHECK) {
|
||||
for (path in pathsToCheck) {
|
||||
val file = File(communityRoot, path)
|
||||
if (!file.isDirectory()) {
|
||||
if (!file.exists()) {
|
||||
printlnErr("Error: Path to check does not exist: $path")
|
||||
anyPathMissing = true
|
||||
}
|
||||
}
|
||||
if (anyPathMissing) exitProcess(1)
|
||||
|
||||
println(
|
||||
"🔍 Comparing commits for paths '${Config.PATHS_TO_CHECK.joinToString()}' between " +
|
||||
"'$branch1' and '$branch2' since $sinceDate..."
|
||||
)
|
||||
// Display comparison parameters
|
||||
println("🔍 Comparing commits between '$branch1' and '$branch2' since $sinceDate for paths:")
|
||||
pathsToCheck.forEach { println(" - $it") }
|
||||
if (jewelOnly) println("[Only including commits explicitly tagged as Jewel]")
|
||||
if (noBuildChanges) println("[Excluding build/ directory changes]")
|
||||
if (buildOnly) println("[Only including build/ directory changes]")
|
||||
println()
|
||||
return pathsToCheck
|
||||
}
|
||||
|
||||
val pathsArg = Config.PATHS_TO_CHECK.joinToString(separator = " ") { "-- $it" }
|
||||
/**
|
||||
* Fetches commits for a given branch, filtering by author date.
|
||||
*
|
||||
* Uses the author date instead of the commit date to properly handle cherry-picks.
|
||||
*/
|
||||
private suspend fun fetchCommits(branch: String, pathsToCheck: List<String>): List<CommitInfo> {
|
||||
val communityRoot = findCommunityRoot()!!
|
||||
val pathsArg = pathsToCheck.joinToString(separator = " ") { "-- $it" }
|
||||
|
||||
val gitLogCommand1 = "git log $branch1 --after=$sinceDate --pretty=format:'${Config.GIT_LOG_FORMAT}' $pathsArg"
|
||||
val commits1 =
|
||||
runCommand(gitLogCommand1, communityRoot)
|
||||
.output
|
||||
.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { parseCommitLine(it) }
|
||||
println("📋 Fetching commits from '$branch'...")
|
||||
|
||||
val gitLogCommand2 = "git log $branch2 --after=$sinceDate --pretty=format:'${Config.GIT_LOG_FORMAT}' $pathsArg"
|
||||
val commits2 =
|
||||
runCommand(gitLogCommand2, communityRoot)
|
||||
.output
|
||||
.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { parseCommitLine(it) }
|
||||
val result =
|
||||
runCommand("git log $branch --pretty=format:'${Config.GIT_LOG_FORMAT},%ai' $pathsArg", communityRoot)
|
||||
val allLines = result.output.lines()
|
||||
val nonBlankLines = allLines.filter { it.isNotBlank() }
|
||||
|
||||
if (verbose) {
|
||||
if (commits1.isNotEmpty()) {
|
||||
println("\n--- Commits for '$branch1' ---")
|
||||
commits1.forEach(::printCommitInfoLine)
|
||||
println()
|
||||
}
|
||||
if (commits2.isNotEmpty()) {
|
||||
println("\n--- Commits for '$branch2' ---")
|
||||
commits2.forEach(::printCommitInfoLine)
|
||||
println()
|
||||
println(" 📊 Raw output: ${allLines.size} lines total, ${nonBlankLines.size} non-blank")
|
||||
}
|
||||
|
||||
// Parse commit messages and filter by author date
|
||||
val commits = mutableListOf<CommitInfo>()
|
||||
val unparseable = mutableListOf<String>()
|
||||
var dateFilteredCount = 0
|
||||
|
||||
nonBlankLines.forEach { line ->
|
||||
when (val result = parseCommitWithDateResult(line)) {
|
||||
is ParseResult.Success -> commits.add(result.commit)
|
||||
is ParseResult.DateFiltered -> dateFilteredCount++
|
||||
is ParseResult.ParseError -> unparseable.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
println(" ✅ Parsed ${commits.size} commits, filtered out $dateFilteredCount commits by date")
|
||||
}
|
||||
|
||||
// Report truly unparseable commits only
|
||||
if (unparseable.isNotEmpty()) {
|
||||
printlnWarn(
|
||||
" ⚠️ Warning: ${unparseable.size} commits from '$branch' could not be parsed due to format issues:"
|
||||
)
|
||||
for (line in unparseable.take(5)) { // Only show the first 5 to avoid spam
|
||||
println(" • ${line.take(100)}${if (line.length > 100) "..." else ""}")
|
||||
}
|
||||
if (unparseable.size > 5) {
|
||||
println(" ... and ${unparseable.size - 5} more")
|
||||
}
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
/** Parses a git log line and returns a ParseResult indicating success, date filtering, or parse error */
|
||||
private fun parseCommitWithDateResult(line: String): ParseResult {
|
||||
// Remove surrounding quotes first
|
||||
val cleanLine = line.trim().removeSurrounding("'")
|
||||
|
||||
// Author dates have a consistent format: YYYY-MM-DD HH:MM:SS +ZZZZ
|
||||
// Use regex to find the author date at the end of the line
|
||||
val authorDateRegex = Regex("""^(.+),(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+\-]\d{4})$""")
|
||||
val matchResult = authorDateRegex.find(cleanLine) ?: return ParseResult.ParseError
|
||||
|
||||
val commitPart = matchResult.groupValues[1]
|
||||
val authorDatePart = matchResult.groupValues[2]
|
||||
|
||||
val commit = parseCommitLine(commitPart)
|
||||
|
||||
// Filter by author date (handles cherry-picks correctly) using proper date parsing
|
||||
return if (isCommitAfterDate(authorDatePart, sinceDate)) {
|
||||
ParseResult.Success(commit)
|
||||
} else {
|
||||
ParseResult.DateFiltered
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares author date with since date using proper datetime parsing. Handles timezone-aware comparison properly.
|
||||
*/
|
||||
private fun isCommitAfterDate(authorDateStr: String, sinceDateStr: String): Boolean {
|
||||
return try {
|
||||
// Parse the git author date (format: "2025-09-25 12:37:32 +0200")
|
||||
val authorDate = ZonedDateTime.parse(authorDateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"))
|
||||
|
||||
// Parse the "since" date (format: "2025-08-25" -> start of day in local timezone)
|
||||
val sinceDate =
|
||||
LocalDate.parse(sinceDateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
||||
.atStartOfDay()
|
||||
.atZone(authorDate.zone) // Use same timezone as author for fair comparison
|
||||
|
||||
authorDate.isAfter(sinceDate) || authorDate.isEqual(sinceDate)
|
||||
} catch (e: Exception) {
|
||||
printlnWarn(
|
||||
" Warning: Failed to parse dates - author: '$authorDateStr', since: '$sinceDateStr' (${e.message})"
|
||||
)
|
||||
// Fallback to string comparison if parsing fails
|
||||
authorDateStr >= sinceDateStr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares commits between branches and displays results. Cherry-picked commits with identical subjects are
|
||||
* considered the same.
|
||||
*/
|
||||
private fun compareAndDisplayResults(commits1: List<CommitInfo>, commits2: List<CommitInfo>) {
|
||||
val horizontalLine = "------------------------------------------------------------------"
|
||||
println(horizontalLine)
|
||||
|
||||
// Find commits unique to branch1 (by comparing normalized subjects)
|
||||
val branch2Subjects = commits2.map { it.subject }.toSet()
|
||||
val commitsInBranch1Only = commits1.filter { (_, subject) -> subject !in branch2Subjects }
|
||||
|
||||
if (verbose) {
|
||||
println("Commits in '$branch1' but not '$branch2':")
|
||||
println(commitsInBranch1Only.joinToString("\n"))
|
||||
println("Analyzing commits in '$branch1' but not '$branch2':")
|
||||
println(" Raw list: ${commitsInBranch1Only.joinToString { it.subject }}")
|
||||
println()
|
||||
}
|
||||
|
||||
@@ -162,12 +313,13 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
println()
|
||||
println(horizontalLine)
|
||||
|
||||
// Find commits unique to branch2
|
||||
val branch1Subjects = commits1.map { it.subject }.toSet()
|
||||
val commitsInBranch2Only = commits2.filter { (_, subject) -> subject !in branch1Subjects }
|
||||
|
||||
if (verbose) {
|
||||
println("All commits in '$branch2' but not '$branch1':")
|
||||
println(commitsInBranch2Only.joinToString("\n"))
|
||||
println("Analyzing commits in '$branch2' but not '$branch1':")
|
||||
println(" Raw list: ${commitsInBranch2Only.joinToString { it.subject }}")
|
||||
println()
|
||||
}
|
||||
|
||||
@@ -182,6 +334,7 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
println(horizontalLine)
|
||||
}
|
||||
|
||||
/** Parses a git log line to extract commit hash and subject */
|
||||
private fun parseCommitLine(commitLine: String): CommitInfo {
|
||||
val (hash, subject) = commitLine.split(Config.DELIMITER)
|
||||
if (subject.isBlank()) {
|
||||
@@ -190,14 +343,17 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
return CommitInfo(hash.trim('\''), normalizeSubject(subject.trim('\'')))
|
||||
}
|
||||
|
||||
/** Prints a formatted commit line with hash and subject, highlighting Jewel commits */
|
||||
private fun printCommitInfoLine(commitInfo: CommitInfo) {
|
||||
val isJewelCommit = commitInfo.subject.contains("jewel", ignoreCase = true)
|
||||
if (jewelOnly && !isJewelCommit) return
|
||||
|
||||
val output = buildString {
|
||||
append(" - ")
|
||||
append(commitInfo.hash.take(7))
|
||||
append(" | ")
|
||||
if (showHashes) {
|
||||
append(commitInfo.hash.take(7))
|
||||
append(" | ")
|
||||
}
|
||||
append(commitInfo.subject)
|
||||
}
|
||||
|
||||
@@ -208,10 +364,12 @@ private class CompareBranchesCommand : SuspendingCliktCommand() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes commit subjects by removing cherry-pick prefixes. This ensures cherry-picked commits match their
|
||||
* original counterparts.
|
||||
*/
|
||||
private fun normalizeSubject(subject: String): String =
|
||||
subject
|
||||
.replaceFirst(Regex("""^(cherry picked from commit [0-9a-f]+):? """, RegexOption.IGNORE_CASE), "")
|
||||
.trim()
|
||||
subject.replaceFirst(Regex("""^(cherry picked from commit [0-9a-f]+):? """, RegexOption.IGNORE_CASE), "").trim()
|
||||
}
|
||||
|
||||
runBlocking { CompareBranchesCommand().main(args) }
|
||||
|
||||
Reference in New Issue
Block a user