From 65045c00ac5ba4fbb72fff2205ad2ba40fece07e Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Wed, 3 Aug 2022 19:18:00 +0200 Subject: [PATCH] [github] rework project repo manager Make it more flexible to allow further code reuse Switch to coroutines GitOrigin-RevId: 6bf3de73c1b1eaf47342eef4fd6d2552119a5e21 --- .../collaboration/async/CoroutineUtil.kt | 47 ++++ plugins/github/resources/META-INF/plugin.xml | 7 - .../github/GHOpenInBrowserActionGroup.kt | 8 +- ...HOpenInBrowserFromAnnotationActionGroup.kt | 4 +- .../plugins/github/GithubCopyPathProvider.kt | 4 +- .../plugins/github/GithubShareAction.kt | 2 +- .../plugins/github/GithubSyncForkAction.java | 4 +- .../GHProtectedBranchRulesLoader.kt | 5 +- .../pullrequest/GHPRToolWindowFactory.kt | 20 +- .../GithubPullRequestsProjectUISettings.kt | 5 +- .../GHPRRepositorySelectorComponentFactory.kt | 41 ++-- .../GHPRToolWindowTabControllerImpl.kt | 46 ++-- .../create/GHPRCreateComponentHolder.kt | 4 +- .../create/GHPRMergeDirectionModelImpl.kt | 4 +- .../util/GHHostedRepositoriesManager.kt | 200 ++++++++++++++++++ .../util/GHProjectRepositoriesManager.kt | 186 ++-------------- .../plugins/github/test/GithubGitRepoTest.kt | 5 +- 17 files changed, 349 insertions(+), 243 deletions(-) create mode 100644 platform/collaboration-tools/src/com/intellij/collaboration/async/CoroutineUtil.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/util/GHHostedRepositoriesManager.kt diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/async/CoroutineUtil.kt b/platform/collaboration-tools/src/com/intellij/collaboration/async/CoroutineUtil.kt new file mode 100644 index 000000000000..2894c1a3fef6 --- /dev/null +++ b/platform/collaboration-tools/src/com/intellij/collaboration/async/CoroutineUtil.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.collaboration.async + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import org.jetbrains.annotations.ApiStatus +import kotlin.coroutines.CoroutineContext + +@ApiStatus.Experimental +@Suppress("FunctionName") +fun DisposingMainScope(parentDisposable: Disposable): CoroutineScope = + MainScope().also { + Disposer.register(parentDisposable) { + it.cancel() + } + } + +@ApiStatus.Experimental +interface ScopedDisposable : Disposable { + val scope: CoroutineScope +} + +@ApiStatus.Experimental +class CancellingScopedDisposable(context: CoroutineContext = SupervisorJob()) : ScopedDisposable { + + override val scope: CoroutineScope = CoroutineScope(context) + + override fun dispose() { + scope.cancel() + } +} + +@ApiStatus.Experimental +fun combineState(scope: CoroutineScope, + state1: StateFlow, + state2: StateFlow, + transform: (T1, T2) -> R): StateFlow = + combine(state1, state2, transform) + .stateIn(scope, SharingStarted.Eagerly, transform(state1.value, state2.value)) \ No newline at end of file diff --git a/plugins/github/resources/META-INF/plugin.xml b/plugins/github/resources/META-INF/plugin.xml index 8a5e004c03ac..0a2e478d8feb 100644 --- a/plugins/github/resources/META-INF/plugin.xml +++ b/plugins/github/resources/META-INF/plugin.xml @@ -76,13 +76,6 @@ - - - - - diff --git a/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserActionGroup.kt b/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserActionGroup.kt index fe42875079c3..67c9341e4f73 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserActionGroup.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserActionGroup.kt @@ -33,7 +33,7 @@ import org.jetbrains.annotations.Nls import org.jetbrains.plugins.github.api.GHRepositoryCoordinates import org.jetbrains.plugins.github.i18n.GithubBundle import org.jetbrains.plugins.github.pullrequest.action.GHPRActionKeys -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import org.jetbrains.plugins.github.util.GithubNotificationIdsHolder import org.jetbrains.plugins.github.util.GithubNotifications import org.jetbrains.plugins.github.util.GithubUtil @@ -87,7 +87,7 @@ open class GHOpenInBrowserActionGroup val repository = GitUtil.getRepositoryManager(project).getRepositoryForFileQuick(fileRevision.path) if (repository == null) return null - val accessibleRepositories = project.service().findKnownRepositories(repository) + val accessibleRepositories = project.service().findKnownRepositories(repository) if (accessibleRepositories.isEmpty()) return null return accessibleRepositories.map { Data.Revision(project, it.ghRepositoryCoordinates, fileRevision.revisionNumber.asString()) } @@ -105,7 +105,7 @@ open class GHOpenInBrowserActionGroup if (repository == null) return null - val accessibleRepositories = project.service().findKnownRepositories(repository) + val accessibleRepositories = project.service().findKnownRepositories(repository) if (accessibleRepositories.isEmpty()) return null return accessibleRepositories.map { Data.Revision(project, it.ghRepositoryCoordinates, commit.hash.asString()) } @@ -118,7 +118,7 @@ open class GHOpenInBrowserActionGroup if (repository == null) return null - val accessibleRepositories = project.service().findKnownRepositories(repository) + val accessibleRepositories = project.service().findKnownRepositories(repository) if (accessibleRepositories.isEmpty()) return null val changeListManager = ChangeListManager.getInstance(project) diff --git a/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserFromAnnotationActionGroup.kt b/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserFromAnnotationActionGroup.kt index e3aef27bdf8f..7d6b496ed522 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserFromAnnotationActionGroup.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GHOpenInBrowserFromAnnotationActionGroup.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.vcs.annotate.UpToDateLineNumberListener import com.intellij.vcsUtil.VcsUtil import git4idea.GitUtil import git4idea.annotate.GitFileAnnotation -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager class GHOpenInBrowserFromAnnotationActionGroup(val annotation: FileAnnotation) @@ -25,7 +25,7 @@ class GHOpenInBrowserFromAnnotationActionGroup(val annotation: FileAnnotation) val filePath = VcsUtil.getFilePath(virtualFile) val repository = GitUtil.getRepositoryManager(project).getRepositoryForFileQuick(filePath) ?: return null - val accessibleRepositories = project.service().findKnownRepositories(repository) + val accessibleRepositories = project.service().findKnownRepositories(repository) if (accessibleRepositories.isEmpty()) return null val revisionHash = annotation.getLineRevisionNumber(myLineNumber)?.asString() diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCopyPathProvider.kt b/plugins/github/src/org/jetbrains/plugins/github/GithubCopyPathProvider.kt index 2ae66dba8a4c..be6e94d66c26 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCopyPathProvider.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCopyPathProvider.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.vcs.FileStatus import com.intellij.openapi.vcs.changes.ChangeListManager import com.intellij.openapi.vfs.VirtualFile import git4idea.GitUtil -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager class GithubCopyPathProvider: DumbAwareCopyPathProvider() { override fun getPathToElement(project: Project, virtualFile: VirtualFile?, editor: Editor?): String? { @@ -21,7 +21,7 @@ class GithubCopyPathProvider: DumbAwareCopyPathProvider() { val repository = GitUtil.getRepositoryManager(project).getRepositoryForFileQuick(virtualFile) if (repository == null) return null - val accessibleRepositories = project.service().findKnownRepositories(repository) + val accessibleRepositories = project.service().findKnownRepositories(repository) if (accessibleRepositories.isEmpty()) return null val refs = accessibleRepositories diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt index b39d66355f13..9128661f23bb 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt @@ -107,7 +107,7 @@ class GithubShareAction : DumbAwareAction(GithubBundle.messagePointer("share.act val gitRepository = GithubGitHelper.findGitRepository(project, file) val possibleRemotes = gitRepository - ?.let(project.service()::findKnownRepositories) + ?.let(project.service()::findKnownRepositories) ?.map { it.gitRemoteUrlCoordinates.url }.orEmpty() if (possibleRemotes.isNotEmpty()) { val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes) diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubSyncForkAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubSyncForkAction.java index 33e2c11d0afc..208b1104718a 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubSyncForkAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubSyncForkAction.java @@ -90,7 +90,7 @@ public class GithubSyncForkAction extends DumbAwareAction { return; } - GHProjectRepositoriesManager ghRepositoriesManager = project.getServiceIfCreated(GHProjectRepositoriesManager.class); + GHHostedRepositoriesManager ghRepositoriesManager = project.getServiceIfCreated(GHHostedRepositoriesManager.class); if (ghRepositoriesManager == null) { LOG.warn("Unable to get the GHProjectRepositoriesManager service"); return; @@ -158,7 +158,7 @@ public class GithubSyncForkAction extends DumbAwareAction { Project project = e.getData(CommonDataKeys.PROJECT); if (project == null || project.isDefault()) return false; - GHProjectRepositoriesManager repositoriesManager = project.getServiceIfCreated(GHProjectRepositoriesManager.class); + GHHostedRepositoriesManager repositoriesManager = project.getServiceIfCreated(GHHostedRepositoriesManager.class); if (repositoriesManager == null) return false; return !repositoriesManager.getKnownRepositories().isEmpty(); diff --git a/plugins/github/src/org/jetbrains/plugins/github/extensions/GHProtectedBranchRulesLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/extensions/GHProtectedBranchRulesLoader.kt index f5ae7ee6a9a1..994a576f46ed 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/extensions/GHProtectedBranchRulesLoader.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/extensions/GHProtectedBranchRulesLoader.kt @@ -18,7 +18,7 @@ import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager import org.jetbrains.plugins.github.api.util.SimpleGHGQLPagesLoader import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager import org.jetbrains.plugins.github.i18n.GithubBundle -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import org.jetbrains.plugins.github.util.GithubProjectSettings private val LOG = logger() @@ -63,7 +63,8 @@ internal class GHProtectedBranchRulesLoader : GitFetchHandler { val requestExecutor = GithubApiRequestExecutorManager.getInstance().getExecutor(account) val githubRepositoryMapping = - project.service().findKnownRepositories(repository).find { it.gitRemoteUrlCoordinates.remote == remote } + project.service().findKnownRepositories(repository) + .find { it.gitRemoteUrlCoordinates.remote == remote } ?: continue val repositoryCoordinates = githubRepositoryMapping.ghRepositoryCoordinates diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GHPRToolWindowFactory.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GHPRToolWindowFactory.kt index ca7bd42353bc..51afbad468c7 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GHPRToolWindowFactory.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GHPRToolWindowFactory.kt @@ -1,10 +1,10 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.github.pullrequest +import com.intellij.collaboration.async.DisposingMainScope import com.intellij.openapi.actionSystem.CommonShortcuts import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.EmptyAction -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project @@ -13,6 +13,7 @@ import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ex.ToolWindowEx import com.intellij.openapi.wm.impl.content.ToolWindowContentUi +import kotlinx.coroutines.launch import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager import org.jetbrains.plugins.github.pullrequest.action.GHPRSelectPullRequestForFileAction import org.jetbrains.plugins.github.pullrequest.action.GHPRSwitchRemoteAction @@ -20,18 +21,17 @@ import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProject import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContextRepository import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.GHPRToolWindowTabController import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.GHPRToolWindowTabControllerImpl -import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import javax.swing.JPanel internal class GHPRToolWindowFactory : ToolWindowFactory, DumbAware { override fun init(toolWindow: ToolWindow) { - ApplicationManager.getApplication().messageBus.connect(toolWindow.disposable) - .subscribe(GHProjectRepositoriesManager.LIST_CHANGES_TOPIC, object : GHProjectRepositoriesManager.ListChangeListener { - override fun repositoryListChanged(newList: Set, project: Project) { - toolWindow.isAvailable = newList.isNotEmpty() - } - }) + val repositoriesManager = toolWindow.project.service() + DisposingMainScope(toolWindow.disposable).launch { + repositoriesManager.knownRepositoriesState.collect { + toolWindow.isAvailable = it.isNotEmpty() + } + } } override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) = with(toolWindow as ToolWindowEx) { @@ -45,7 +45,7 @@ internal class GHPRToolWindowFactory : ToolWindowFactory, DumbAware { setDisposer(Disposer.newDisposable("GHPR tab disposable")) }.also { val authManager = GithubAuthenticationManager.getInstance() - val repositoryManager = project.service() + val repositoryManager = project.service() val dataContextRepository = GHPRDataContextRepository.getInstance(project) val projectString = GithubPullRequestsProjectUISettings.getInstance(project) it.putUserData(GHPRToolWindowTabController.KEY, diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt index 775e8676bc21..2960357bc3e2 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt @@ -3,14 +3,13 @@ package org.jetbrains.plugins.github.pullrequest.config import com.intellij.openapi.components.* import com.intellij.openapi.project.Project -import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.github.api.GHRepositoryCoordinates import org.jetbrains.plugins.github.api.GHRepositoryPath import org.jetbrains.plugins.github.api.GithubServerPath import org.jetbrains.plugins.github.authentication.accounts.GHAccountSerializer import org.jetbrains.plugins.github.authentication.accounts.GithubAccount import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager @Service @State(name = "GithubPullRequestsUISettings", storages = [Storage(StoragePathMacros.WORKSPACE_FILE)], reportStatistic = false) @@ -27,7 +26,7 @@ class GithubPullRequestsProjectUISettings(private val project: Project) var selectedRepoAndAccount: Pair? get() { val (url, accountId) = state.selectedUrlAndAccountId ?: return null - val repo = project.service().knownRepositories.find { + val repo = project.service().knownRepositories.find { it.gitRemoteUrlCoordinates.url == url } ?: return null val account = GHAccountSerializer.deserialize(accountId) ?: return null diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRRepositorySelectorComponentFactory.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRRepositorySelectorComponentFactory.kt index fc3fe3836611..d3ea26b74f33 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRRepositorySelectorComponentFactory.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRRepositorySelectorComponentFactory.kt @@ -1,11 +1,11 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.github.pullrequest.ui.toolwindow +import com.intellij.collaboration.async.DisposingMainScope import com.intellij.collaboration.auth.AccountsListener import com.intellij.collaboration.ui.CollaborationToolsUIUtil.defaultButton import com.intellij.ide.plugins.newui.HorizontalLayout import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.project.Project import com.intellij.ui.components.ActionLink @@ -13,6 +13,7 @@ import com.intellij.util.castSafelyTo import com.intellij.util.ui.JBUI import com.intellij.util.ui.UI import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.launch import net.miginfocom.layout.CC import net.miginfocom.layout.LC import net.miginfocom.layout.PlatformDefaults @@ -26,8 +27,7 @@ import org.jetbrains.plugins.github.ui.component.GHAccountSelectorComponentFacto import org.jetbrains.plugins.github.ui.component.GHRepositorySelectorComponentFactory import org.jetbrains.plugins.github.ui.util.getName import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager.ListChangeListener +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import java.awt.event.ActionEvent import javax.swing.* import javax.swing.event.ListDataEvent @@ -35,7 +35,7 @@ import javax.swing.event.ListDataListener class GHPRRepositorySelectorComponentFactory(private val project: Project, private val authManager: GithubAuthenticationManager, - private val repositoryManager: GHProjectRepositoriesManager) { + private val repositoryManager: GHHostedRepositoriesManager) { fun create(disposable: Disposable, onSelected: (GHGitRepositoryMapping, GithubAccount) -> Unit): JComponent { val repositoriesModel = ComboBoxWithActionsModel().apply { //todo: add remote action @@ -72,7 +72,7 @@ class GHPRRepositorySelectorComponentFactory(private val project: Project, } } - Controller(project = project, authManager = authManager, repositoryManager = repositoryManager, + Controller(project = project, authManager = authManager, repositoriesManager = repositoryManager, repositoriesModel = repositoriesModel, accountsModel = accountsModel, applyAction = applyAction, githubLoginAction = githubLoginAction, tokenLoginAction = tokenLoginAction, gheLoginActon = gheLoginAction, @@ -126,7 +126,7 @@ class GHPRRepositorySelectorComponentFactory(private val project: Project, private class Controller(private val project: Project, private val authManager: GithubAuthenticationManager, - private val repositoryManager: GHProjectRepositoriesManager, + private val repositoriesManager: GHHostedRepositoriesManager, private val repositoriesModel: ComboBoxWithActionsModel, private val accountsModel: ComboBoxWithActionsModel, private val applyAction: Action, @@ -135,33 +135,30 @@ class GHPRRepositorySelectorComponentFactory(private val project: Project, private val gheLoginActon: Action, disposable: Disposable) { + private val scope = DisposingMainScope(disposable) + init { - ApplicationManager.getApplication().messageBus.connect(disposable) - .subscribe(GHProjectRepositoriesManager.LIST_CHANGES_TOPIC, object : ListChangeListener { - override fun repositoryListChanged(newList: Set, project: Project) { - updateRepositories() - } - }) + repositoriesModel.addSelectionChangeListener(::updateAccounts) + repositoriesModel.addSelectionChangeListener(::updateActions) + accountsModel.addSelectionChangeListener(::updateActions) + + scope.launch { + repositoriesManager.knownRepositoriesState.collect { + repositoriesModel.items = it.toList() + repositoriesModel.preSelect() + } + } + authManager.addListener(disposable, object : AccountsListener { override fun onAccountListChanged(old: Collection, new: Collection) { invokeAndWaitIfNeeded(runnable = ::updateAccounts) } }) - repositoriesModel.addSelectionChangeListener(::updateAccounts) - repositoriesModel.addSelectionChangeListener(::updateActions) - accountsModel.addSelectionChangeListener(::updateActions) - - updateRepositories() updateAccounts() updateActions() } - private fun updateRepositories() { - repositoriesModel.items = repositoryManager.knownRepositories.sortedBy { it.gitRemoteUrlCoordinates.remote.name } - repositoriesModel.preSelect() - } - private fun updateAccounts() { val serverPath = repositoriesModel.selectedItem?.wrappee?.ghRepositoryCoordinates?.serverPath if (serverPath == null) { diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt index fb896a63c613..50b6a93c8904 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt @@ -1,11 +1,11 @@ // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.plugins.github.pullrequest.ui.toolwindow +import com.intellij.collaboration.async.DisposingMainScope import com.intellij.collaboration.auth.AccountsListener import com.intellij.ide.DataManager import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.ClearableLazyValue import com.intellij.openapi.util.Disposer @@ -14,6 +14,11 @@ import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.content.Content import com.intellij.util.IJSwingUtilities import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch import org.jetbrains.plugins.github.api.GithubApiRequestExecutor import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestShort @@ -32,14 +37,14 @@ import org.jetbrains.plugins.github.pullrequest.ui.GHLoadingPanelFactory import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRCreateComponentHolder import org.jetbrains.plugins.github.ui.util.GHUIUtil import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import java.awt.BorderLayout import javax.swing.JComponent import kotlin.properties.Delegates internal class GHPRToolWindowTabControllerImpl(private val project: Project, private val authManager: GithubAuthenticationManager, - private val repositoryManager: GHProjectRepositoriesManager, + private val repositoryManager: GHHostedRepositoriesManager, private val dataContextRepository: GHPRDataContextRepository, private val projectSettings: GithubPullRequestsProjectUISettings, private val tab: Content) : GHPRToolWindowTabController { @@ -54,6 +59,12 @@ internal class GHPRToolWindowTabControllerImpl(private val project: Project, private val tabDisposable = Disposer.newCheckedDisposable().also { Disposer.register(tab.disposer!!, it) } + private val scope = DisposingMainScope(tabDisposable) + + private val resetRequestFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST).apply { + tryEmit(Unit) + } + private var contentDisposable by Delegates.observable(null) { _, oldValue, newValue -> if (oldValue != null) Disposer.dispose(oldValue) if (newValue != null) Disposer.register(tabDisposable, newValue) @@ -71,22 +82,29 @@ internal class GHPRToolWindowTabControllerImpl(private val project: Project, } init { + val accountsFlow = MutableSharedFlow>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) authManager.addListener(tabDisposable, object : AccountsListener { - override fun onAccountListChanged(old: Collection, new: Collection) = scheduleUpdate() - override fun onAccountCredentialsChanged(account: GithubAccount) = scheduleUpdate() + override fun onAccountListChanged(old: Collection, new: Collection) { + accountsFlow.tryEmit(new.toSet()) + } - private fun scheduleUpdate() = ApplicationManager.getApplication() - .invokeLater(Runnable { Updater().update() }) { tabDisposable.isDisposed } + override fun onAccountCredentialsChanged(account: GithubAccount) { + accountsFlow.tryEmit(authManager.getAccounts()) + } }) - repositoryManager.addRepositoryListChangedListener(tabDisposable) { - Updater().update() + accountsFlow.tryEmit(authManager.getAccounts()) + + scope.launch { + combine(repositoryManager.knownRepositoriesState, accountsFlow, resetRequestFlow) { repos, accounts, _ -> + Updater(repos, accounts) + }.collectLatest { + it.update() + } } - Updater().update() } - private inner class Updater { - private val repos = repositoryManager.knownRepositories - private val accounts = authManager.getAccounts() + private inner class Updater(private val repos: Set, + private val accounts: Set) { fun update() { val wasReset = resetIfMissing() @@ -230,7 +248,7 @@ internal class GHPRToolWindowTabControllerImpl(private val project: Project, currentRepository = null currentAccount = null projectSettings.selectedRepoAndAccount = null - Updater().update() + resetRequestFlow.tryEmit(Unit) } private inner class ComponentController(private val dataContext: GHPRDataContext, diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRCreateComponentHolder.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRCreateComponentHolder.kt index e2f1353d80ba..edb400836a2e 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRCreateComponentHolder.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRCreateComponentHolder.kt @@ -53,7 +53,7 @@ import org.jetbrains.plugins.github.ui.util.DisableableDocument import org.jetbrains.plugins.github.util.ChangeDiffRequestProducerFactory import org.jetbrains.plugins.github.util.DiffRequestChainProducer import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import java.util.concurrent.CompletableFuture import javax.swing.JComponent import javax.swing.JPanel @@ -64,7 +64,7 @@ import javax.swing.text.PlainDocument internal class GHPRCreateComponentHolder(private val actionManager: ActionManager, private val project: Project, private val settings: GithubPullRequestsProjectUISettings, - private val repositoriesManager: GHProjectRepositoriesManager, + private val repositoriesManager: GHHostedRepositoriesManager, private val dataContext: GHPRDataContext, private val viewController: GHPRToolWindowTabComponentController, disposable: Disposable) { diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRMergeDirectionModelImpl.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRMergeDirectionModelImpl.kt index 7a6846b92b82..d9e277ba507e 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRMergeDirectionModelImpl.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/create/GHPRMergeDirectionModelImpl.kt @@ -7,11 +7,11 @@ import git4idea.GitBranch import git4idea.GitRemoteBranch import git4idea.ui.branch.MergeDirectionModel import org.jetbrains.plugins.github.util.GHGitRepositoryMapping -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import org.jetbrains.plugins.github.util.GithubUtil.Delegates.observableField class GHPRMergeDirectionModelImpl(override val baseRepo: GHGitRepositoryMapping, - private val repositoriesManager: GHProjectRepositoriesManager) : MergeDirectionModel { + private val repositoriesManager: GHHostedRepositoriesManager) : MergeDirectionModel { private val changeEventDispatcher = EventDispatcher.create(SimpleEventListener::class.java) diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GHHostedRepositoriesManager.kt b/plugins/github/src/org/jetbrains/plugins/github/util/GHHostedRepositoriesManager.kt new file mode 100644 index 000000000000..e421202dfe2f --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GHHostedRepositoriesManager.kt @@ -0,0 +1,200 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.github.util + +import com.intellij.collaboration.async.CancellingScopedDisposable +import com.intellij.collaboration.async.ScopedDisposable +import com.intellij.collaboration.async.combineState +import com.intellij.collaboration.auth.AccountsListener +import com.intellij.collaboration.hosting.GitHostingUrlUtil +import com.intellij.dvcs.repo.VcsRepositoryManager +import com.intellij.dvcs.repo.VcsRepositoryMappingListener +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryChangeListener +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import org.jetbrains.plugins.github.api.GithubServerPath +import org.jetbrains.plugins.github.authentication.accounts.GHAccountManager +import org.jetbrains.plugins.github.authentication.accounts.GithubAccount +import org.jetbrains.plugins.github.pullrequest.GHPRStatisticsCollector + +@Service +class GHHostedRepositoriesManager(private val project: Project) : ScopedDisposable by CancellingScopedDisposable() { + + private val accountManager: GHAccountManager get() = service() + private val repositoryManager: GitRepositoryManager get() = project.service() + private val metadataLoader: GHEnterpriseServerMetadataLoader get() = service() + + val knownRepositoriesState: StateFlow> + val knownRepositories: Set + get() = knownRepositoriesState.value + + init { + val accountsServersFlow = createAccountsServersFlow().also { + checkServerVersions(it) + } + + val unitTestMode = ApplicationManager.getApplication().isUnitTestMode + if (unitTestMode) { + checkServerVersions(accountsServersFlow) + } + + val remotesFlow = createRemotesFlow() + val discoveredServersState = if (unitTestMode) { + MutableStateFlow(emptySet()) + } + else { + startServerDiscovery(accountsServersFlow, remotesFlow) + } + + val knownServersFlow = combineState(scope, accountsServersFlow, discoveredServersState, ::collectServers) + + knownRepositoriesState = combineState(scope, knownServersFlow, remotesFlow, ::collectMappings) + } + + private fun createAccountsServersFlow(): StateFlow> { + val flow = MutableStateFlow>(emptySet()) + accountManager.addListener(this, object : AccountsListener { + override fun onAccountListChanged(old: Collection, new: Collection) { + flow.value = new.map { it.server }.toSet() + } + }) + flow.value = accountManager.accounts.map { it.server }.toSet() + return flow + } + + private fun createRemotesFlow(): StateFlow> { + val flow = MutableStateFlow(collectRemotes()) + project.messageBus.connect(this).subscribe(VcsRepositoryManager.VCS_REPOSITORY_MAPPING_UPDATED, VcsRepositoryMappingListener { + flow.update { collectRemotes() } + }) + project.messageBus.connect(this).subscribe(GitRepository.GIT_REPO_CHANGE, GitRepositoryChangeListener { + flow.update { collectRemotes() } + }) + // update after initial bc can happen outside EDT + flow.update { collectRemotes() } + return flow + } + + private fun collectRemotes(): Set { + val gitRepositories = repositoryManager.repositories + if (gitRepositories.isEmpty()) { + LOG.debug("No repositories found") + return emptySet() + } + + return gitRepositories.flatMap { repo -> + repo.remotes.flatMap { remote -> + remote.urls.mapNotNull { url -> + GitRemoteUrlCoordinates(url, remote, repo) + } + } + }.toSet() + } + + private fun startServerDiscovery(accountsServersFlow: StateFlow>, + remotesFlow: StateFlow>): StateFlow> { + val stateFlow = MutableStateFlow>(emptySet()) + + scope.launch { + combine(accountsServersFlow, remotesFlow) { servers, remotes -> + remotes.filter { remote -> servers.none { it.matches(remote.url) } } + }.collect { remotes -> + remotes.chunked(URLS_CHECK_PARALLELISM).forEach { remotesChunk -> + remotesChunk.map { remote -> + async { + val server = checkForEnterpriseServer(remote) + if (server != null) stateFlow.update { it + server } + } + }.awaitAll() + } + } + } + return stateFlow + } + + private fun collectServers(accountsServers: Set, discoveredServers: Set): Set { + val servers = mutableSetOf(GithubServerPath.DEFAULT_SERVER) + servers.addAll(accountsServers) + servers.addAll(discoveredServers) + return servers + } + + private fun collectMappings(servers: Set, remotes: Collection): Set { + val mappings = HashSet() + for (remote in remotes) { + val repository = servers.find { it.matches(remote.url) }?.let { GHGitRepositoryMapping.create(it, remote) } + if (repository != null) { + mappings.add(repository) + } + } + LOG.debug("New list of known repos: $mappings") + return mappings + } + + fun findKnownRepositories(repository: GitRepository): List { + return knownRepositoriesState.value.filter { + it.gitRemoteUrlCoordinates.repository == repository + } + } + + private suspend fun checkForEnterpriseServer(remote: GitRemoteUrlCoordinates): GithubServerPath? { + val uri = GitHostingUrlUtil.getUriFromRemoteUrl(remote.url) + LOG.debug("Extracted URI $uri from remote ${remote.url}") + if (uri == null) return null + + val host = uri.host ?: return null + val path = uri.path ?: return null + val pathParts = path.removePrefix("/").split('/').takeIf { it.size >= 2 } ?: return null + val serverSuffix = if (pathParts.size == 2) null else pathParts.subList(0, pathParts.size - 2).joinToString("/", "/") + + for (server in listOf( + GithubServerPath(false, host, null, serverSuffix), + GithubServerPath(true, host, null, serverSuffix), + GithubServerPath(true, host, 8080, serverSuffix) + )) { + LOG.debug("Looking for GHE server at $server") + try { + metadataLoader.loadMetadata(server).await() + LOG.debug("Found GHE server at $server") + return server + } + catch (ignored: Throwable) { + } + } + return null + } + + private fun checkServerVersions(serversFlow: Flow>) { + scope.launch { + serversFlow.collectLatest { servers -> + for (server in servers) { + if (server.isGithubDotCom) { + continue + } + + try { + val metadata = metadataLoader.loadMetadata(server).await() + GHPRStatisticsCollector.logEnterpriseServerMeta(project, server, metadata) + } + catch (ignore: Exception) { + } + } + } + } + } + + companion object { + private val LOG = logger() + + private const val URLS_CHECK_PARALLELISM = 10 + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GHProjectRepositoriesManager.kt b/plugins/github/src/org/jetbrains/plugins/github/util/GHProjectRepositoriesManager.kt index a3eeea1f4535..ed78fb9de76c 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/util/GHProjectRepositoriesManager.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GHProjectRepositoriesManager.kt @@ -1,198 +1,48 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.github.util -import com.intellij.collaboration.async.CompletableFutureUtil.errorOnEdt -import com.intellij.collaboration.async.CompletableFutureUtil.successOnEdt -import com.intellij.collaboration.auth.AccountsListener -import com.intellij.collaboration.hosting.GitHostingUrlUtil -import com.intellij.dvcs.repo.VcsRepositoryMappingListener import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project -import com.intellij.util.SingleAlarm -import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.messages.Topic -import git4idea.repo.GitRepository -import git4idea.repo.GitRepositoryChangeListener -import git4idea.repo.GitRepositoryManager -import org.jetbrains.annotations.CalledInAny -import org.jetbrains.plugins.github.api.GithubServerPath -import org.jetbrains.plugins.github.authentication.accounts.GHAccountManager -import org.jetbrains.plugins.github.authentication.accounts.GithubAccount -import org.jetbrains.plugins.github.pullrequest.GHPRStatisticsCollector -import kotlin.properties.Delegates.observable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +@Deprecated("Use GHHostedRepositoriesManager") @Service class GHProjectRepositoriesManager(private val project: Project) : Disposable { - private val updateAlarm = SingleAlarm(task = ::doUpdateRepositories, delay = 50, parentDisposable = this) - private val accountManager: GHAccountManager - get() = service() + private val scope = CoroutineScope(SupervisorJob()) + private val repositoriesManager: GHHostedRepositoriesManager get() = project.service() - var knownRepositories by observable(emptySet()) { _, oldValue, newValue -> - if (oldValue != newValue) { - ApplicationManager.getApplication().messageBus.syncPublisher(LIST_CHANGES_TOPIC).repositoryListChanged(newValue, project) - } - } - private set - - private val serversFromDiscovery = HashSet() + val knownRepositories: Set + get() = repositoriesManager.knownRepositoriesState.value init { - accountManager.addListener(this, object : AccountsListener { - override fun onAccountListChanged(old: Collection, new: Collection) = runInEdt { - updateRepositories() - } - }) - updateRepositories() - } - - fun findKnownRepositories(repository: GitRepository): List { - return knownRepositories.filter { - it.gitRemoteUrlCoordinates.repository == repository - } - } - - @CalledInAny - private fun updateRepositories() { - if (ApplicationManager.getApplication().isUnitTestMode) { - doUpdateRepositories() - } - else { - updateAlarm.request() - } - } - - //TODO: execute on pooled thread - need to make GithubAccountManager ready - @RequiresEdt - private fun doUpdateRepositories() { - LOG.debug("Repository list update started") - val gitRepositories = project.service().repositories - if (gitRepositories.isEmpty()) { - knownRepositories = emptySet() - LOG.debug("No repositories found") - return - } - - val remotes = gitRepositories.flatMap { repo -> - repo.remotes.flatMap { remote -> - remote.urls.mapNotNull { url -> - GitRemoteUrlCoordinates(url, remote, repo) - } + scope.launch { + repositoriesManager.knownRepositoriesState.collect { + ApplicationManager.getApplication().messageBus.syncPublisher(LIST_CHANGES_TOPIC).repositoryListChanged(it, project) } } - LOG.debug("Found remotes: $remotes") - - val authenticatedServers = accountManager.accounts.map { it.server } - val servers = mutableListOf().apply { - add(GithubServerPath.DEFAULT_SERVER) - addAll(authenticatedServers) - addAll(serversFromDiscovery) - } - - val repositories = HashSet() - for (remote in remotes) { - val repository = servers.find { it.matches(remote.url) }?.let { GHGitRepositoryMapping.create(it, remote) } - if (repository != null) { - repositories.add(repository) - } - else { - scheduleEnterpriseServerDiscovery(remote) - } - } - LOG.debug("New list of known repos: $repositories") - knownRepositories = repositories - - for (server in authenticatedServers) { - if (server.isGithubDotCom) { - continue - } - service().loadMetadata(server).successOnEdt { - GHPRStatisticsCollector.logEnterpriseServerMeta(project, server, it) - } - } - } - - @RequiresEdt - private fun scheduleEnterpriseServerDiscovery(remote: GitRemoteUrlCoordinates) { - val uri = GitHostingUrlUtil.getUriFromRemoteUrl(remote.url) - LOG.debug("Extracted URI $uri from remote ${remote.url}") - if (uri == null) return - - val host = uri.host ?: return - val path = uri.path ?: return - val pathParts = path.removePrefix("/").split('/').takeIf { it.size >= 2 } ?: return - val serverSuffix = if (pathParts.size == 2) null else pathParts.subList(0, pathParts.size - 2).joinToString("/", "/") - - val server = GithubServerPath(false, host, null, serverSuffix) - val serverHttp = GithubServerPath(true, host, null, serverSuffix) - val server8080 = GithubServerPath(true, host, 8080, serverSuffix) - LOG.debug("Scheduling GHE server discovery for $server, $serverHttp and $server8080") - - val serverManager = service() - serverManager.loadMetadata(server).successOnEdt { - LOG.debug("Found GHE server at $server") - serversFromDiscovery.add(server) - invokeLater(runnable = ::doUpdateRepositories) - }.errorOnEdt { - serverManager.loadMetadata(serverHttp).successOnEdt { - LOG.debug("Found GHE server at $serverHttp") - serversFromDiscovery.add(serverHttp) - invokeLater(runnable = ::doUpdateRepositories) - }.errorOnEdt { - serverManager.loadMetadata(server8080).successOnEdt { - LOG.debug("Found GHE server at $server8080") - serversFromDiscovery.add(server8080) - invokeLater(runnable = ::doUpdateRepositories) - } - } - } - } - - fun addRepositoryListChangedListener(disposable: Disposable, listener: () -> Unit) { - ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(LIST_CHANGES_TOPIC, object : ListChangeListener { - override fun repositoryListChanged(newList: Set, project: Project) = listener() - }) - } - - internal class RemoteUrlsListener(private val project: Project) : VcsRepositoryMappingListener, GitRepositoryChangeListener { - override fun mappingChanged() = runInEdt(project) { updateRepositories(project) } - override fun repositoryChanged(repository: GitRepository) = runInEdt(project) { updateRepositories(project) } } interface ListChangeListener { fun repositoryListChanged(newList: Set, project: Project) } - companion object { - private val LOG = logger() + override fun dispose() { + scope.cancel() + } + @Deprecated("Use GHHostedRepositoriesManager", level = DeprecationLevel.ERROR) + companion object { + @Deprecated("Use GHHostedRepositoriesManager.knownRepositoriesState") @JvmField @Topic.AppLevel val LIST_CHANGES_TOPIC = Topic(ListChangeListener::class.java, Topic.BroadcastDirection.NONE) - - private inline fun runInEdt(project: Project, crossinline runnable: () -> Unit) { - val application = ApplicationManager.getApplication() - if (application.isDispatchThread) runnable() - else application.invokeLater({ runnable() }) { project.isDisposed } - } - - private fun updateRepositories(project: Project) { - try { - if (!project.isDisposed) { - project.service().updateRepositories() - } - } - catch (e: Exception) { - LOG.info("Error occurred while updating repositories", e) - } - } } - - override fun dispose() {} } \ No newline at end of file diff --git a/plugins/github/test/org/jetbrains/plugins/github/test/GithubGitRepoTest.kt b/plugins/github/test/org/jetbrains/plugins/github/test/GithubGitRepoTest.kt index e8f1b874d912..428f99f7f341 100644 --- a/plugins/github/test/org/jetbrains/plugins/github/test/GithubGitRepoTest.kt +++ b/plugins/github/test/org/jetbrains/plugins/github/test/GithubGitRepoTest.kt @@ -10,7 +10,7 @@ import git4idea.config.GitConfigUtil import git4idea.repo.GitRepository import git4idea.test.GitHttpAuthTestService import git4idea.test.git -import org.jetbrains.plugins.github.util.GHProjectRepositoriesManager +import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager import org.jetbrains.plugins.github.util.GithubUtil abstract class GithubGitRepoTest : GithubTest() { @@ -60,7 +60,8 @@ abstract class GithubGitRepoTest : GithubTest() { protected fun checkRemoteConfigured() { assertNotNull(repository) - assertTrue("GitHub remote is not configured", project.service().knownRepositories.any { + val mappings = project.service().knownRepositories + assertTrue("GitHub remote is not configured, current mappings: $mappings", mappings.any { it.gitRemoteUrlCoordinates.repository == repository }) }