diff --git a/plugins/space/src/main/kotlin/circlet/actions/CircletMainToolBarAction.kt b/plugins/space/src/main/kotlin/circlet/actions/CircletMainToolBarAction.kt index 002fa6e5384c..d37bce878078 100644 --- a/plugins/space/src/main/kotlin/circlet/actions/CircletMainToolBarAction.kt +++ b/plugins/space/src/main/kotlin/circlet/actions/CircletMainToolBarAction.kt @@ -12,7 +12,6 @@ import circlet.ui.AccountsMenuListPopup import circlet.vcs.* import circlet.vcs.clone.* import circlet.workspaces.* -import com.intellij.ide.* import com.intellij.openapi.actionSystem.* import com.intellij.openapi.project.* import com.intellij.openapi.ui.popup.* @@ -133,30 +132,28 @@ class CircletMainToolBarAction : DumbAwareAction() { { CircletCloneAction.runClone(project) }, showSeparatorAbove = true) val projectContext = CircletProjectContext.getInstance(project) - val projectInfos = projectContext.projectDescriptions - - if (projectInfos != null) { - val descriptions = projectInfos.second + val context = projectContext.context.value + if (!context.empty) { + val descriptions = context.reposInProject.keys if (descriptions.size > 1) { menuItems += AccountMenuItem.Group("Code Reviews", descriptions.map { - val reviewsUrl = Navigator.p.project(it.projectKey).reviews.absoluteHref(host) - browseAction("Open for ${it.project.name} project",reviewsUrl) + val reviewsUrl = Navigator.p.project(it.key).reviews.absoluteHref(host) + browseAction("Open for ${it.project.name} project", reviewsUrl) }.toList()) menuItems += AccountMenuItem.Group("Checklists", descriptions.map { - val checklistsUrl = Navigator.p.project(it.projectKey).checklists().absoluteHref(host) - browseAction("Open for ${it.project.name} project",checklistsUrl) + val checklistsUrl = Navigator.p.project(it.key).checklists().absoluteHref(host) + browseAction("Open for ${it.project.name} project", checklistsUrl) }.toList()) menuItems += AccountMenuItem.Group("Issues", descriptions.map { - val issuesUrl = Navigator.p.project(it.projectKey).issues().absoluteHref(host) - browseAction("Open for ${it.project.name} project",issuesUrl) + val issuesUrl = Navigator.p.project(it.key).issues().absoluteHref(host) + browseAction("Open for ${it.project.name} project", issuesUrl) }.toList()) - } - else { - val p = Navigator.p.project(descriptions.first().projectKey) + } else { + val p = Navigator.p.project(descriptions.first().key) - menuItems += browseAction("Code Reviews",p.reviews.absoluteHref(host)) + menuItems += browseAction("Code Reviews", p.reviews.absoluteHref(host)) menuItems += browseAction("Checklists", p.checklists().absoluteHref(host)) menuItems += browseAction("Issues", p.issues().absoluteHref(host)) } diff --git a/plugins/space/src/main/kotlin/circlet/vcs/CircletOpenInBrowserAction.kt b/plugins/space/src/main/kotlin/circlet/vcs/CircletOpenInBrowserAction.kt index 6e3bda55522b..2af05a97055c 100644 --- a/plugins/space/src/main/kotlin/circlet/vcs/CircletOpenInBrowserAction.kt +++ b/plugins/space/src/main/kotlin/circlet/vcs/CircletOpenInBrowserAction.kt @@ -26,7 +26,7 @@ import git4idea.GitUtil import git4idea.history.GitHistoryUtils import git4idea.repo.GitRepository import icons.SpaceIcons -import runtime.routing.Location +import runtime.routing.* import com.intellij.openapi.util.Ref as Ref1 abstract class CircletOpenInBrowserActionGroup(groupName: String) : @@ -57,15 +57,15 @@ abstract class CircletOpenInBrowserActionGroup(groupName: String) : } abstract class CircletOpenInBrowserAction(groupName: String) : - CircletOpenInBrowserActionGroup>(groupName) { + CircletOpenInBrowserActionGroup>(groupName) { override fun update(e: AnActionEvent) { CircletActionUtils.showIconInActionSearch(e) val project = e.project if (project != null) { val projectContext = CircletProjectContext.getInstance(project) - val projectDescriptions = projectContext.projectDescriptions - if (projectDescriptions != null) { + val projectDescriptions = !projectContext.context.value.empty + if (projectDescriptions) { e.presentation.isEnabled = true return @@ -74,19 +74,19 @@ abstract class CircletOpenInBrowserAction(groupName: String) : e.presentation.isEnabled = false } - override fun buildAction(it: Pair): AnAction = - object : AnAction("Open for ${it.first.projectKey.key} project") { + override fun buildAction(it: Pair): AnAction = + object : AnAction("Open for ${it.first.key.key} project") { override fun actionPerformed(e: AnActionEvent) = BrowserUtil.browse(it.second) } companion object { - internal fun getProjectAwareUrls(endpoint: (ProjectLocation) -> Location, context: DataContext): List>? { + internal fun getProjectAwareUrls(endpoint: (ProjectLocation) -> Location, context: DataContext): List>? { val project = context.getData(CommonDataKeys.PROJECT) ?: return null val server = circletWorkspace.workspace.value?.client?.server?.removeSuffix("/") ?: return null - val description = CircletProjectContext.getInstance(project).projectDescriptions ?: return null + val description = CircletProjectContext.getInstance(project).context.value - return description.second.map { - val projectLocation = Navigator.p.project(it.projectKey) + return description.reposInProject.keys.map { + val projectLocation = Navigator.p.project(it.key) val url = endpoint(projectLocation).absoluteHref(server) it to url @@ -96,19 +96,19 @@ abstract class CircletOpenInBrowserAction(groupName: String) : } class OpenReviews : CircletOpenInBrowserAction("Code reviews") { - override fun getData(dataContext: DataContext): List>? { + override fun getData(dataContext: DataContext): List>? { return getProjectAwareUrls(ProjectLocation::reviews, dataContext) } } class OpenChecklists : CircletOpenInBrowserAction("Checklists") { - override fun getData(dataContext: DataContext): List>? { + override fun getData(dataContext: DataContext): List>? { return getProjectAwareUrls(ProjectLocation::checklists, dataContext) } } class OpenIssues : CircletOpenInBrowserAction("Issues") { - override fun getData(dataContext: DataContext): List>? { + override fun getData(dataContext: DataContext): List>? { return getProjectAwareUrls(ProjectLocation::issues, dataContext) } } @@ -143,9 +143,9 @@ class CircletVcsOpenInBrowserActionGroup : val change = changeListManager.getChange(virtualFile) if (change != null && change.type == Change.Type.NEW) return null - val repoKeysInfo = findProjectInfo(gitRepository, project) ?: return null + val repoDescription = findProjectInfo(gitRepository, project) ?: return null - return repoKeysInfo.second.map { OpenData.File(project, server, it, repoKeysInfo.first, virtualFile, gitRepository) } + return repoDescription.projectInfos.map { OpenData.File(project, server, it, repoDescription.name, virtualFile, gitRepository) } } @@ -154,26 +154,26 @@ class CircletVcsOpenInBrowserActionGroup : val fileRevision = dataContext.getData(VcsDataKeys.VCS_FILE_REVISION) ?: return null if (fileRevision !is VcsFileRevisionEx) return null val gitRepository = GitUtil.getRepositoryManager(project).getRepositoryForFileQuick(filePath) ?: return null - val repoKeysInfo = findProjectInfo(gitRepository, project) ?: return null + val repoDescription = findProjectInfo(gitRepository, project) ?: return null - return repoKeysInfo.second.map { OpenData.FileRevision(project, server, it, repoKeysInfo.first, fileRevision) } + return repoDescription.projectInfos.map { OpenData.FileRevision(project, server, it, repoDescription.name, fileRevision) } } private fun getDataFromLog(dataContext: DataContext, project: Project, server: String): List? { val vcsLog = dataContext.getData(VcsLogDataKeys.VCS_LOG) ?: return null val selectedCommit = vcsLog.selectedCommits.firstOrNull() ?: return null val gitRepository = GitUtil.getRepositoryManager(project).getRepositoryForFileQuick(selectedCommit.root) ?: return null - val repoKeysInfo = findProjectInfo(gitRepository, project) ?: return null + val repoDescription = findProjectInfo(gitRepository, project) ?: return null - return repoKeysInfo.second.map { OpenData.Commit(project, server, it, repoKeysInfo.first, selectedCommit) } + return repoDescription.projectInfos.map { OpenData.Commit(project, server, it, repoDescription.name, selectedCommit) } } - private fun findProjectInfo(gitRepository: GitRepository, project: Project): Pair>? { + private fun findProjectInfo(gitRepository: GitRepository, project: Project): CircletRepoInfo? { val circletContext = CircletProjectContext.getInstance(project) return getRemoteUrls(gitRepository) - .mapNotNull { circletContext.findProjectInfo(it) } - .firstOrNull() ?: return null + .mapNotNull { circletContext.getRepoDescriptionByUrl(it) } + .firstOrNull() } private fun getRemoteUrls(gitRepository: GitRepository): List { @@ -181,7 +181,7 @@ class CircletVcsOpenInBrowserActionGroup : } companion object { - class OpenAction(private val data: OpenData) : DumbAwareAction("Open for ${data.description.project.name} project") { + class OpenAction(private val data: OpenData) : DumbAwareAction("Open for ${data.info.project.name} project") { override fun actionPerformed(e: AnActionEvent) { data.url?.let { BrowserUtil.browse(it) } @@ -190,20 +190,20 @@ class CircletVcsOpenInBrowserActionGroup : sealed class OpenData(val project: Project, val server: String, - val description: CircletProjectDescription, + val info: CircletProjectInfo, val repo: String ) { abstract val url: String? class Commit(project: Project, server: String, - projectKey: CircletProjectDescription, + projectKey: CircletProjectInfo, repo: String, private val commit: CommitId) : OpenData(project, server, projectKey, repo) { override val url: String? get() { - return Navigator.p.project(description.projectKey) + return Navigator.p.project(info.key) .commits(repo, "", commit.hash.asString()) .absoluteHref(server) } @@ -211,13 +211,13 @@ class CircletVcsOpenInBrowserActionGroup : class FileRevision(project: Project, server: String, - projectKey: CircletProjectDescription, + projectKey: CircletProjectInfo, repo: String, private val vcsFileRevisionEx: VcsFileRevisionEx) : OpenData(project, server, projectKey, repo) { override val url: String? get() { - return Navigator.p.project(description.projectKey) + return Navigator.p.project(info.key) .revision(repo, vcsFileRevisionEx.revisionNumber.asString()) .absoluteHref(server) } @@ -225,7 +225,7 @@ class CircletVcsOpenInBrowserActionGroup : class File(project: Project, server: String, - projectKey: CircletProjectDescription, + projectKey: CircletProjectInfo, repo: String, private val virtualFile: VirtualFile, private val gitRepository: GitRepository) : OpenData(project, server, projectKey, repo) { override val url: String? @@ -233,7 +233,7 @@ class CircletVcsOpenInBrowserActionGroup : val relativePath = VfsUtilCore.getRelativePath(virtualFile, gitRepository.root) ?: return null val hash = getCurrentFileRevisionHash(project, virtualFile) ?: return null - return Navigator.p.project(description.projectKey) + return Navigator.p.project(info.key) .fileAnnotate(repo, hash, relativePath) .absoluteHref(server) } diff --git a/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectContext.kt b/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectContext.kt index 46852d7fde21..8e6aa0b0dc02 100644 --- a/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectContext.kt +++ b/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectContext.kt @@ -1,16 +1,22 @@ package circlet.vcs -import circlet.client.* -import circlet.client.api.* -import circlet.components.* -import circlet.platform.client.* -import circlet.workspaces.* -import com.intellij.openapi.* -import com.intellij.openapi.project.* -import git4idea.* -import git4idea.repo.* -import libraries.coroutines.extra.* -import runtime.async.* +import circlet.client.api.PR_Project +import circlet.client.api.ProjectKey +import circlet.client.api.Projects +import circlet.client.api.RepositoryService +import circlet.client.pr +import circlet.client.repoService +import circlet.components.circletWorkspace +import circlet.platform.client.ConnectionStatus +import circlet.platform.client.resolve +import circlet.workspaces.Workspace +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import git4idea.GitUtil +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryChangeListener +import libraries.coroutines.extra.LifetimeSource +import runtime.async.backoff import runtime.reactive.* class CircletProjectContext(project: Project) : Disposable { @@ -18,15 +24,12 @@ class CircletProjectContext(project: Project) : Disposable { private val remoteUrls: MutableProperty> = Property.createMutable(findRemoteUrls(project)) - private val projectsInfo: Property>?>> = lifetime.mapInit(circletWorkspace.workspace, remoteUrls, emptyMap()) { ws, urls -> - ws ?: return@mapInit emptyMap>?>() + val context: Property = lifetime.mapInit(circletWorkspace.workspace, remoteUrls, EMPTY) { ws, urls -> + ws ?: return@mapInit EMPTY ws.client.connectionStatus.filter { it is ConnectionStatus.Connected }.awaitFirst(ws.lifetime) reloadProjectKeys(ws, urls) } - val projectDescriptions: Pair>? - get() = projectsInfo.value.values.filterNotNull().firstOrNull() - init { project.messageBus .connect(this) @@ -36,8 +39,8 @@ class CircletProjectContext(project: Project) : Disposable { }) } - fun findProjectInfo(remoteUrl: String): Pair>? { - return projectsInfo.value[remoteUrl] + fun getRepoDescriptionByUrl(remoteUrl: String): CircletRepoInfo? { + return context.value.repoByUrl[remoteUrl] } private fun findRemoteUrls(project: Project): Set = GitUtil.getRepositoryManager(project).repositories @@ -45,20 +48,33 @@ class CircletProjectContext(project: Project) : Disposable { .flatMap { it.urls } .toSet() - private suspend fun reloadProjectKeys(ws: Workspace, urls: Set): Map>?> { - return urls.map { url -> + private suspend fun reloadProjectKeys(ws: Workspace, urls: Set): Context { + val reposByUrl: Map = urls.map { url -> backoff { url to loadProjectKeysForUrl(ws, url) } }.toMap() + val reposInProject = HashMap>() + + for (repoDescription in reposByUrl.values.filterNotNull()) { + for (projectDescription in repoDescription.projectInfos) { + reposInProject.getOrPut(projectDescription, { HashSet() }) + .add(repoDescription) + + } + } + + return Context(reposByUrl, reposInProject) } - private suspend fun loadProjectKeysForUrl(ws: Workspace, url: String): Pair>? { + private suspend fun loadProjectKeysForUrl(ws: Workspace, url: String): CircletRepoInfo? { val repoService: RepositoryService = ws.client.repoService - val repoProjectKeys = repoService.findByRepositoryUrl(url) ?: return null + val (repoName, projectKeys) = repoService.findByRepositoryUrl(url) ?: return null val projectService: Projects = ws.client.pr - val list = repoProjectKeys.second.map { CircletProjectDescription(it, projectService.getProjectByKey(it).resolve()) }.toList() - return repoProjectKeys.first to list + val projectDescriptions = projectKeys.map { CircletProjectInfo(it, projectService.getProjectByKey(it).resolve()) }.toSet() + return CircletRepoInfo(url, + repoName, + projectDescriptions) } override fun dispose() { @@ -71,3 +87,27 @@ class CircletProjectContext(project: Project) : Disposable { } } } + +data class Context( + val repoByUrl: Map, + + val reposInProject: Map> +) { + val empty: Boolean + get() { + return this == EMPTY + } +} + +private val EMPTY: Context = Context(emptyMap(), emptyMap()) + +data class CircletRepoInfo( + val url: String, + val name: String, + val projectInfos: Set +) + +data class CircletProjectInfo( + val key: ProjectKey, + val project: PR_Project +) diff --git a/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectDescription.kt b/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectDescription.kt deleted file mode 100644 index 1d0ffe2c9d3a..000000000000 --- a/plugins/space/src/main/kotlin/circlet/vcs/CircletProjectDescription.kt +++ /dev/null @@ -1,8 +0,0 @@ -package circlet.vcs - -import circlet.client.api.* - -data class CircletProjectDescription( - val projectKey: ProjectKey, - val project: PR_Project -) diff --git a/plugins/space/src/main/kotlin/circlet/vcs/share/CircletShareProjectAction.kt b/plugins/space/src/main/kotlin/circlet/vcs/share/CircletShareProjectAction.kt index c5ab748ea864..5b068c8f6f9b 100644 --- a/plugins/space/src/main/kotlin/circlet/vcs/share/CircletShareProjectAction.kt +++ b/plugins/space/src/main/kotlin/circlet/vcs/share/CircletShareProjectAction.kt @@ -44,8 +44,8 @@ class CircletShareProjectAction : DumbAwareAction() { } val context = CircletProjectContext.getInstance(project) - val descriptions = context.projectDescriptions - if (descriptions != null && descriptions.second.isNotEmpty()) { + val descriptions = context.context.value.empty + if (descriptions) { e.presentation.isEnabledAndVisible = false return }