Skip to content

Commit

Permalink
Introduce a type for CodebaseName (#996)
Browse files Browse the repository at this point in the history
Fixes #967.

## Test plan
1. tbd
  • Loading branch information
mkondratek committed Mar 7, 2024
1 parent 1136d1e commit f974182
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 55 deletions.
17 changes: 10 additions & 7 deletions src/main/java/com/sourcegraph/vcs/ConvertUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package com.sourcegraph.vcs

import java.net.URI

fun convertGitCloneURLToCodebaseNameOrError(cloneURL: String): String {
data class CodebaseName(val value: String)

fun convertGitCloneURLToCodebaseNameOrError(theCloneURL: String): CodebaseName {
val cloneURL = theCloneURL.lowercase()

// Handle common Git SSH URL format
val sshUrlRegexMatchResult = Regex("""^[\w-]+@([^:]+):([\w-]+)/([\w-.]+)$""").find(cloneURL)
if (sshUrlRegexMatchResult != null) {
val (host, owner, repo) = sshUrlRegexMatchResult.destructured
return "$host/$owner/${repo.replace(".git$".toRegex(), "")}"
return CodebaseName("$host/$owner/${repo.replace(".git$".toRegex(), "")}")
}

var uri = URI(cloneURL)
Expand All @@ -18,29 +21,29 @@ fun convertGitCloneURLToCodebaseNameOrError(cloneURL: String): String {

// Handle Azure DevOps URLs
if (uri.host?.contains("dev.azure") == true && !uri.path.isNullOrEmpty()) {
return "${uri.host}${uri.path.replace("/_git", "")}"
return CodebaseName("${uri.host}${uri.path.replace("/_git", "")}")
}

// Handle GitHub URLs
if (uri.scheme?.startsWith("github") == true) {
return "github.com/${uri.schemeSpecificPart.replace(".git", "")}"
return CodebaseName("github.com/${uri.schemeSpecificPart.replace(".git", "")}")
}

// Handle GitLab URLs
if (uri.scheme?.startsWith("gitlab") == true) {
return "gitlab.com/${uri.schemeSpecificPart.replace(".git", "")}"
return CodebaseName("gitlab.com/${uri.schemeSpecificPart.replace(".git", "")}")
}

// Handle HTTPS URLs
if (uri.scheme?.startsWith("http") == true &&
!uri.host.isNullOrEmpty() &&
!uri.path.isNullOrEmpty()) {
return "${uri.host}${uri.path?.replace(".git", "")}"
return CodebaseName("${uri.host}${uri.path?.replace(".git", "")}")
}

// Generic URL
if (uri.host != null && !uri.path.isNullOrEmpty()) {
return "${uri.host}${uri.path.replace(".git", "")}"
return CodebaseName("${uri.host}${uri.path.replace(".git", "")}")
}

throw Exception("Cody could not extract repo name from clone URL $cloneURL")
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/sourcegraph/vcs/RepoUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private static String doReplacements(

if (vcsType == VCSType.GIT && repository != null) {
String cloneURL = GitUtil.getRemoteRepoUrl((GitRepository) repository, project);
return convertGitCloneURLToCodebaseNameOrError(cloneURL);
return convertGitCloneURLToCodebaseNameOrError(cloneURL).getValue();
}

if (vcsType == VCSType.PERFORCE) {
Expand Down
37 changes: 37 additions & 0 deletions src/main/kotlin/com/sourcegraph/cody/config/SettingsMigration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import com.sourcegraph.cody.CodyToolWindowFactory
import com.sourcegraph.cody.api.SourcegraphApiRequestExecutor
import com.sourcegraph.cody.api.SourcegraphApiRequests
import com.sourcegraph.cody.history.HistoryService
import com.sourcegraph.cody.history.state.ChatState
import com.sourcegraph.cody.history.state.EnhancedContextState
import com.sourcegraph.cody.history.state.LLMState
import com.sourcegraph.cody.initialization.Activity
import com.sourcegraph.config.AccessTokenStorage
import com.sourcegraph.config.CodyApplicationService
import com.sourcegraph.config.CodyProjectService
import com.sourcegraph.config.ConfigUtil
import com.sourcegraph.config.UserLevelConfig
import com.sourcegraph.vcs.convertGitCloneURLToCodebaseNameOrError
import java.util.concurrent.CompletableFuture

class SettingsMigration : Activity {
Expand All @@ -35,6 +38,9 @@ class SettingsMigration : Activity {
migrateAccounts(project)
}
RunOnceUtil.runOnceForProject(project, "CodyHistoryLlmMigration") { migrateLlms(project) }
RunOnceUtil.runOnceForProject(project, "CodyConvertUrlToCodebaseName") {
migrateUrlsToCodebaseNames(project)
}
RunOnceUtil.runOnceForApp("CodyApplicationSettingsMigration") { migrateApplicationSettings() }
RunOnceUtil.runOnceForApp("ToggleCodyToolWindowAfterMigration") {
toggleCodyToolbarWindow(project)
Expand Down Expand Up @@ -105,6 +111,19 @@ class SettingsMigration : Activity {
}
}

private fun migrateUrlsToCodebaseNames(project: Project) {
val enhancedContextState =
HistoryService.getInstance(project).state.defaultEnhancedContext ?: return

migrateUrlsToCodebaseNames(enhancedContextState)

HistoryService.getInstance(project)
.state
.chats
.mapNotNull(ChatState::enhancedContext)
.forEach(Companion::migrateUrlsToCodebaseNames)
}

private val modelToProviderAndTitle =
mapOf(
"Claude 2 by Anthropic" to Triple("anthropic/claude-2", "Anthropic", "claude 2"),
Expand Down Expand Up @@ -391,5 +410,23 @@ class SettingsMigration : Activity {

companion object {
private val LOG = logger<SettingsMigration>()

fun migrateUrlsToCodebaseNames(enhancedContextState: EnhancedContextState) {
val remoteRepositories =
enhancedContextState.remoteRepositories
.onEach { remoteRepositoryState ->
runCatching {
remoteRepositoryState.remoteUrl?.let { remoteUrl ->
convertGitCloneURLToCodebaseNameOrError(remoteUrl)
}
}
.getOrNull()
?.let { remoteRepositoryState.codebaseName = it.value }
}
.filter { it.codebaseName != null }
.distinctBy { it.codebaseName }
.toMutableList()
enhancedContextState.remoteRepositories = remoteRepositories
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import com.intellij.openapi.project.Project
import com.sourcegraph.cody.agent.CodyAgentService
import com.sourcegraph.cody.agent.protocol.GetRepoIdsParam
import com.sourcegraph.cody.agent.protocol.Repo
import com.sourcegraph.vcs.CodebaseName
import java.util.concurrent.CompletableFuture

object RemoteRepoUtils {
fun getRepository(project: Project, codebaseName: String): CompletableFuture<Repo?> {
fun getRepository(project: Project, codebaseName: CodebaseName): CompletableFuture<Repo?> {
val result = CompletableFuture<Repo?>()
CodyAgentService.withAgent(project) { agent ->
try {
val repos = agent.server.getRepoIds(GetRepoIdsParam(listOf(codebaseName), 1)).get()
val repos = agent.server.getRepoIds(GetRepoIdsParam(listOf(codebaseName.value), 1)).get()
result.complete(repos?.repos?.firstOrNull())
} catch (e: Exception) {
result.complete(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.intellij.util.Alarm
import com.sourcegraph.cody.config.DialogValidationUtils
import com.sourcegraph.cody.context.RemoteRepoUtils
import com.sourcegraph.common.CodyBundle
import com.sourcegraph.vcs.CodebaseName
import com.sourcegraph.vcs.convertGitCloneURLToCodebaseNameOrError
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
Expand All @@ -23,7 +24,7 @@ import org.jetbrains.annotations.NotNull
class AddRepositoryDialog(
private val project: Project,
private val remoteContextNode: ContextTreeRemoteRootNode,
private val addAction: (String) -> Unit
private val addAction: (CodebaseName) -> Unit
) : DialogWrapper(project) {

private val repoUrlInputField = TextFieldWithAutoCompletion.create(project, listOf(), false, null)
Expand All @@ -36,20 +37,20 @@ class AddRepositoryDialog(
}

override fun doValidateAll(): List<ValidationInfo> {
val text = repoUrlInputField.text.lowercase()

fun validateNonEmpty() =
DialogValidationUtils.custom(
repoUrlInputField,
CodyBundle.getString("context-panel.add-repo-dialog.error-empty-url")) {
repoUrlInputField.text.isNotBlank()
text.isNotBlank()
}

fun validateValidUrl() =
DialogValidationUtils.custom(
repoUrlInputField,
CodyBundle.getString("context-panel.add-repo-dialog.error-invalid-url")) {
val url =
if (repoUrlInputField.text.startsWith("http")) repoUrlInputField.text
else "http://" + repoUrlInputField.text
val url = if (text.startsWith("http")) text else "http://$text"
runCatching { URL(url) }.isSuccess
}

Expand All @@ -58,8 +59,7 @@ class AddRepositoryDialog(
repoUrlInputField,
CodyBundle.getString("context-panel.add-repo-dialog.error-no-repo")) {
val codebaseName =
runCatching { convertGitCloneURLToCodebaseNameOrError(repoUrlInputField.text) }
.getOrNull()
runCatching { convertGitCloneURLToCodebaseNameOrError(text) }.getOrNull()
codebaseName ?: return@custom false
val repo =
RemoteRepoUtils.getRepository(project, codebaseName)
Expand All @@ -73,13 +73,12 @@ class AddRepositoryDialog(
repoUrlInputField,
CodyBundle.getString("context-panel.add-repo-dialog.error-repo-already-added")) {
val codebaseName =
runCatching { convertGitCloneURLToCodebaseNameOrError(repoUrlInputField.text) }
.getOrNull()
runCatching { convertGitCloneURLToCodebaseNameOrError(text) }.getOrNull()
remoteContextNode
.children()
.toList()
.filterIsInstance<ContextTreeRemoteRepoNode>()
.none { it.codebaseName == codebaseName?.lowercase() }
.none { it.codebaseName == codebaseName }
}

return listOfNotNull(
Expand All @@ -94,7 +93,9 @@ class AddRepositoryDialog(
}

override fun doOKAction() {
addAction(repoUrlInputField.text.lowercase())
runCatching { convertGitCloneURLToCodebaseNameOrError(repoUrlInputField.text.lowercase()) }
.getOrNull()
?.let { codebaseName -> addAction(codebaseName) }
close(OK_EXIT_CODE, true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ class ContextRepositoriesCheckboxRenderer : CheckboxTree.CheckboxTreeCellRendere

when (node) {
is ContextTreeRemoteRepoNode -> {
val repoName = node.codebaseName.split(File.separator).lastOrNull() ?: node.codebaseName
val repoName =
node.codebaseName.value.split(File.separator).lastOrNull() ?: node.codebaseName.value
textRenderer.appendHTML(
"<b>${repoName}</b> <i ${style}>${node.codebaseName}</i>",
"<b>${repoName}</b> <i ${style}>${node.codebaseName.value}</i>",
SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
is ContextTreeLocalRepoNode -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.sourcegraph.cody.context.ui

import com.intellij.openapi.project.Project
import com.intellij.ui.CheckedTreeNode
import com.sourcegraph.vcs.CodebaseName
import java.util.concurrent.atomic.AtomicBoolean

open class ContextTreeNode<T>(value: T, private val onSetChecked: (Boolean) -> Unit = {}) :
Expand All @@ -17,8 +18,8 @@ class ContextTreeRootNode(val text: String, onSetChecked: (Boolean) -> Unit) :

class ContextTreeRemoteRootNode(val text: String) : ContextTreeNode<String>(text)

class ContextTreeRemoteRepoNode(val codebaseName: String, onSetChecked: (Boolean) -> Unit) :
ContextTreeNode<String>(codebaseName, onSetChecked)
class ContextTreeRemoteRepoNode(val codebaseName: CodebaseName, onSetChecked: (Boolean) -> Unit) :
ContextTreeNode<CodebaseName>(codebaseName, onSetChecked)

open class ContextTreeLocalNode<T>(value: T, private val isEnhancedContextEnabled: AtomicBoolean) :
ContextTreeNode<T>(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.sourcegraph.cody.history.HistoryService
import com.sourcegraph.cody.history.state.EnhancedContextState
import com.sourcegraph.cody.history.state.RemoteRepositoryState
import com.sourcegraph.common.CodyBundle
import com.sourcegraph.vcs.CodebaseName
import com.sourcegraph.vcs.convertGitCloneURLToCodebaseNameOrError
import java.awt.Dimension
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -84,7 +85,9 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
if (!isDotComAccount()) {
if (contextState != null) {
contextState.remoteRepositories.forEach { repo ->
repo.remoteUrl?.let { remoteUrl -> addRemoteRepository(remoteUrl, repo.isEnabled) }
repo.codebaseName?.let { codebaseName ->
addRemoteRepository(CodebaseName(codebaseName), repo.isEnabled)
}
}
} else {
CodyAgentCodebase.getInstance(project).getUrl().thenApply { repoUrl ->
Expand All @@ -93,7 +96,9 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
.completeOnTimeout(null, 15, TimeUnit.SECONDS)
.thenApply { repo ->
if (repo != null) {
ApplicationManager.getApplication().invokeLater { addRemoteRepository(repoUrl) }
ApplicationManager.getApplication().invokeLater {
addRemoteRepository(codebaseName)
}
}
}
}
Expand Down Expand Up @@ -121,15 +126,16 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
private fun isDotComAccount() =
CodyAuthenticationManager.instance.getActiveAccount(project)?.isDotcomAccount() ?: false

private fun getRepoByUrlAndRun(codebaseName: String, consumer: Consumer<Repo>) {
private fun getRepoByUrlAndRun(codebaseName: CodebaseName, consumer: Consumer<Repo>) {
RemoteRepoUtils.getRepository(project, codebaseName).thenApply {
it?.let { repo -> consumer.accept(repo) }
}
}

private fun enableRemote(codebaseName: String) {
private fun enableRemote(codebaseName: CodebaseName) {
updateContextState { contextState ->
contextState.remoteRepositories.find { it.remoteUrl == codebaseName }?.isEnabled = true
contextState.remoteRepositories.find { it.codebaseName == codebaseName.value }?.isEnabled =
true
}
getRepoByUrlAndRun(codebaseName) { repo ->
chatSession.sendWebviewMessage(
Expand All @@ -139,9 +145,10 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
}

@RequiresEdt
private fun disableRemote(codebaseName: String) {
private fun disableRemote(codebaseName: CodebaseName) {
updateContextState { contextState ->
contextState.remoteRepositories.find { it.remoteUrl == codebaseName }?.isEnabled = false
contextState.remoteRepositories.find { it.codebaseName == codebaseName.value }?.isEnabled =
false
}
getRepoByUrlAndRun(codebaseName) { repo ->
chatSession.sendWebviewMessage(
Expand All @@ -152,7 +159,7 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
@RequiresEdt
private fun removeRemoteRepository(node: ContextTreeRemoteRepoNode) {
updateContextState { contextState ->
contextState.remoteRepositories.removeIf { it.remoteUrl == node.codebaseName }
contextState.remoteRepositories.removeIf { it.codebaseName == node.codebaseName.value }
}
remoteContextNode.remove(node)
if (enhancedContextNode.children().toList().contains(remoteContextNode) &&
Expand All @@ -164,14 +171,13 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
}

@RequiresEdt
private fun addRemoteRepository(repoUrl: String, isCheckedInitially: Boolean = true) {
val codebaseName = convertGitCloneURLToCodebaseNameOrError(repoUrl)
private fun addRemoteRepository(codebaseName: CodebaseName, isCheckedInitially: Boolean = true) {

updateContextState { contextState ->
val repositories = contextState.remoteRepositories
val existingRepo = repositories.find { it.remoteUrl == codebaseName }
val existingRepo = repositories.find { it.codebaseName == codebaseName.value }
val modifiedRepo = existingRepo ?: RemoteRepositoryState()
modifiedRepo.remoteUrl = codebaseName
modifiedRepo.codebaseName = codebaseName.value
modifiedRepo.isEnabled = isCheckedInitially
if (existingRepo == null) repositories.add(modifiedRepo)
}
Expand Down Expand Up @@ -224,7 +230,9 @@ class EnhancedContextPanel(private val project: Project, private val chatSession
toolbarDecorator.setAddActionName(
CodyBundle.getString("context-panel.button.add-remote-repo"))
toolbarDecorator.setAddAction {
AddRepositoryDialog(project, remoteContextNode) { repoUrl -> addRemoteRepository(repoUrl) }
AddRepositoryDialog(project, remoteContextNode) { codebaseName ->
addRemoteRepository(codebaseName)
}
.show()
expandAllNodes()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ class RemoteRepositoryState : BaseState() {
@get:OptionTag(tag = "isEnabled", nameAttribute = "") var isEnabled: Boolean by property(true)

@get:OptionTag(tag = "remoteUrl", nameAttribute = "") var remoteUrl: String? by string()

@get:OptionTag(tag = "codebaseName", nameAttribute = "") var codebaseName: String? by string()
}
4 changes: 2 additions & 2 deletions src/main/resources/CodyBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ context-panel.tree.root=repositories-root
context-panel.tree.node-chat-context=Context for this Chat
context-panel.tree.node-local-project=Local Project
context-panel.tree.node-remote-repos=Remote Repo(s)
context-panel.add-repo-dialog.title=Add Remote Repo
context-panel.add-repo-dialog.title=Add Remote Repository
context-panel.add-repo-dialog.error-empty-url=Remote repository URL cannot be empty
context-panel.add-repo-dialog.error-invalid-url=Remote repository URL must be valid
context-panel.add-repo-dialog.error-no-repo=Remote repository not found on the server
context-panel.add-repo-dialog.error-repo-already-added=Remote repository has been added already
context-panel.add-repo-dialog.url-input-label=<html><b>Repo URL</b><html>
context-panel.add-repo-dialog.url-input-label=<html><b>Repository URL</b><html>
context-panel.add-repo-dialog.url-input-help=<html><span>Example: <b>github.com/sourcegraph/cody</b><br>Only repos indexed on customer instance are valid.<br><br>Contact your Sourcegraph admin to add missing repo.<span><html>
messages-panel.welcome-text=Hello! I'm Cody. I can write code and answer questions for you. See [Cody documentation](https://sourcegraph.com/docs/cody) for help and tips.
chat-session.error-title=Error while processing the message
Expand Down
Loading

0 comments on commit f974182

Please sign in to comment.