From 19988ccaae5d0eb9162dc14df139564160bb0ce9 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 13 May 2026 16:01:07 -0400 Subject: [PATCH] fix(jetbrains): harden cloud history git remote lookup --- .../client/session/history/GitRemoteUrl.kt | 21 ++++++++++++--- .../session/history/HistoryController.kt | 26 ++++++++++++++----- .../session/history/HistoryControllerTest.kt | 18 +++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt index ad614060638..7093a5008b3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt @@ -1,5 +1,9 @@ package ai.kilocode.client.session.history +import java.io.File +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + /** * Resolves the origin remote URL for a given directory by running * `git remote get-url origin` as a subprocess. @@ -11,9 +15,20 @@ package ai.kilocode.client.session.history */ internal fun resolveGitRemoteUrl(dir: String): String? = runCatching { val proc = ProcessBuilder("git", "remote", "get-url", "origin") - .directory(java.io.File(dir)) + .directory(File(dir)) + .redirectErrorStream(true) .start() - val out = proc.inputStream.bufferedReader().readText().trim() - val code = proc.waitFor() + + val text = CompletableFuture.supplyAsync { + proc.inputStream.bufferedReader().use { it.readText() } + } + val done = proc.waitFor(5, TimeUnit.SECONDS) + if (!done) { + proc.destroyForcibly() + return@runCatching null + } + + val out = text.get(1, TimeUnit.SECONDS).trim() + val code = proc.exitValue() if (code == 0 && out.isNotEmpty()) out else null }.getOrNull() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 3fed465de6f..d4b6165e4d5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -8,7 +8,11 @@ import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.application.ApplicationManager import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class HistoryController( private val sessions: KiloSessionService, @@ -30,6 +34,10 @@ class HistoryController( var gitUrl: String? = null private set + @Volatile + private var resolved = false + private val lock = Mutex() + /** Whether to filter cloud history by the current repository. */ var repoOnly: Boolean = false private set @@ -151,14 +159,18 @@ class HistoryController( * propagates state updates to EDT via [edt]. */ private suspend fun resolveUrlIfNeeded(): String? { - if (gitUrl != null) return gitUrl - val url = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { - gitUrlProvider() + if (resolved) return gitUrl + return lock.withLock { + if (resolved) return@withLock gitUrl + val url = withContext(Dispatchers.IO) { + gitUrlProvider() + } + // Write gitUrl directly (volatile) so it is visible before EDT callbacks fire. + gitUrl = url + resolved = true + if (url != null) updateRepoOnly(true) + url } - // Write gitUrl directly (volatile) so it is visible before EDT callbacks fire. - gitUrl = url - if (url != null) updateRepoOnly(true) - return url } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 2f9d3cb960d..7cddaef3a4d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -27,6 +27,7 @@ import java.awt.Cursor import java.awt.event.KeyEvent import java.time.Instant import java.time.temporal.ChronoUnit +import java.util.concurrent.atomic.AtomicInteger import javax.swing.JComponent import javax.swing.KeyStroke import javax.swing.event.ListDataEvent @@ -573,6 +574,23 @@ class HistoryControllerTest : BasePlatformTestCase() { assertNull(controller.gitUrl) } + fun `test cloud git url resolves once across overlapping reloads`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val calls = AtomicInteger() + val controller = HistoryController(sessions, workspace, scope, gitUrlProvider = { + calls.incrementAndGet() + Thread.sleep(100) + "git@github.com:test/repo.git" + }) + + controller.reloadCloud() + controller.reloadCloud() + flush() + + assertEquals(1, calls.get()) + assertEquals(2, rpc.cloudCalls.size) + } + fun `test cloud load passes null when repo only disabled`() { rpc.cloud += cloud("cloud_1", "Cloud One") val url = "git@github.com:test/repo.git"