[github] rework project repo manager

Make it more flexible to allow further code reuse
Switch to coroutines

GitOrigin-RevId: 6bf3de73c1b1eaf47342eef4fd6d2552119a5e21
This commit is contained in:
Ivan Semenov
2022-08-09 11:15:10 +00:00
committed by intellij-monorepo-bot
parent 762fb7287b
commit 65045c00ac
17 changed files with 349 additions and 243 deletions
@@ -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 <T1, T2, R> combineState(scope: CoroutineScope,
state1: StateFlow<T1>,
state2: StateFlow<T2>,
transform: (T1, T2) -> R): StateFlow<R> =
combine(state1, state2, transform)
.stateIn(scope, SharingStarted.Eagerly, transform(state1.value, state2.value))
@@ -76,13 +76,6 @@
<registryKey defaultValue="true" description="Applying suggested changes" key="github.suggested.changes.apply"/>
</extensions>
<projectListeners>
<listener class="org.jetbrains.plugins.github.util.GHProjectRepositoriesManager$RemoteUrlsListener"
topic="com.intellij.dvcs.repo.VcsRepositoryMappingListener"/>
<listener class="org.jetbrains.plugins.github.util.GHProjectRepositoriesManager$RemoteUrlsListener"
topic="git4idea.repo.GitRepositoryChangeListener"/>
</projectListeners>
<extensions defaultExtensionNs="Git4Idea">
<GitHttpAuthDataProvider implementation="org.jetbrains.plugins.github.extensions.GHHttpAuthDataProvider"/>
<GitHttpAuthDataProvider implementation="org.jetbrains.plugins.github.extensions.GHComHttpAuthDataProvider"/>
@@ -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<GHProjectRepositoriesManager>().findKnownRepositories(repository)
val accessibleRepositories = project.service<GHHostedRepositoriesManager>().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<GHProjectRepositoriesManager>().findKnownRepositories(repository)
val accessibleRepositories = project.service<GHHostedRepositoriesManager>().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<GHProjectRepositoriesManager>().findKnownRepositories(repository)
val accessibleRepositories = project.service<GHHostedRepositoriesManager>().findKnownRepositories(repository)
if (accessibleRepositories.isEmpty()) return null
val changeListManager = ChangeListManager.getInstance(project)
@@ -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<GHProjectRepositoriesManager>().findKnownRepositories(repository)
val accessibleRepositories = project.service<GHHostedRepositoriesManager>().findKnownRepositories(repository)
if (accessibleRepositories.isEmpty()) return null
val revisionHash = annotation.getLineRevisionNumber(myLineNumber)?.asString()
@@ -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<GHProjectRepositoriesManager>().findKnownRepositories(repository)
val accessibleRepositories = project.service<GHHostedRepositoriesManager>().findKnownRepositories(repository)
if (accessibleRepositories.isEmpty()) return null
val refs = accessibleRepositories
@@ -107,7 +107,7 @@ class GithubShareAction : DumbAwareAction(GithubBundle.messagePointer("share.act
val gitRepository = GithubGitHelper.findGitRepository(project, file)
val possibleRemotes = gitRepository
?.let(project.service<GHProjectRepositoriesManager>()::findKnownRepositories)
?.let(project.service<GHHostedRepositoriesManager>()::findKnownRepositories)
?.map { it.gitRemoteUrlCoordinates.url }.orEmpty()
if (possibleRemotes.isNotEmpty()) {
val existingRemotesDialog = GithubExistingRemotesDialog(project, possibleRemotes)
@@ -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();
@@ -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<GHProtectedBranchRulesLoader>()
@@ -63,7 +63,8 @@ internal class GHProtectedBranchRulesLoader : GitFetchHandler {
val requestExecutor = GithubApiRequestExecutorManager.getInstance().getExecutor(account)
val githubRepositoryMapping =
project.service<GHProjectRepositoriesManager>().findKnownRepositories(repository).find { it.gitRemoteUrlCoordinates.remote == remote }
project.service<GHHostedRepositoriesManager>().findKnownRepositories(repository)
.find { it.gitRemoteUrlCoordinates.remote == remote }
?: continue
val repositoryCoordinates = githubRepositoryMapping.ghRepositoryCoordinates
@@ -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<GHGitRepositoryMapping>, project: Project) {
toolWindow.isAvailable = newList.isNotEmpty()
}
})
val repositoriesManager = toolWindow.project.service<GHHostedRepositoriesManager>()
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<GHProjectRepositoriesManager>()
val repositoryManager = project.service<GHHostedRepositoriesManager>()
val dataContextRepository = GHPRDataContextRepository.getInstance(project)
val projectString = GithubPullRequestsProjectUISettings.getInstance(project)
it.putUserData(GHPRToolWindowTabController.KEY,
@@ -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<GHGitRepositoryMapping, GithubAccount>?
get() {
val (url, accountId) = state.selectedUrlAndAccountId ?: return null
val repo = project.service<GHProjectRepositoriesManager>().knownRepositories.find {
val repo = project.service<GHHostedRepositoriesManager>().knownRepositories.find {
it.gitRemoteUrlCoordinates.url == url
} ?: return null
val account = GHAccountSerializer.deserialize(accountId) ?: return null
@@ -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<GHGitRepositoryMapping>().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<GHGitRepositoryMapping>,
private val accountsModel: ComboBoxWithActionsModel<GithubAccount>,
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<GHGitRepositoryMapping>, 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<GithubAccount> {
override fun onAccountListChanged(old: Collection<GithubAccount>, new: Collection<GithubAccount>) {
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) {
@@ -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<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST).apply {
tryEmit(Unit)
}
private var contentDisposable by Delegates.observable<Disposable?>(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<Set<GithubAccount>>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
authManager.addListener(tabDisposable, object : AccountsListener<GithubAccount> {
override fun onAccountListChanged(old: Collection<GithubAccount>, new: Collection<GithubAccount>) = scheduleUpdate()
override fun onAccountCredentialsChanged(account: GithubAccount) = scheduleUpdate()
override fun onAccountListChanged(old: Collection<GithubAccount>, new: Collection<GithubAccount>) {
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<GHGitRepositoryMapping>,
private val accounts: Set<GithubAccount>) {
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,
@@ -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) {
@@ -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<GHGitRepositoryMapping> {
private val repositoriesManager: GHHostedRepositoriesManager) : MergeDirectionModel<GHGitRepositoryMapping> {
private val changeEventDispatcher = EventDispatcher.create(SimpleEventListener::class.java)
@@ -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<Set<GHGitRepositoryMapping>>
val knownRepositories: Set<GHGitRepositoryMapping>
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<Set<GithubServerPath>> {
val flow = MutableStateFlow<Set<GithubServerPath>>(emptySet())
accountManager.addListener(this, object : AccountsListener<GithubAccount> {
override fun onAccountListChanged(old: Collection<GithubAccount>, new: Collection<GithubAccount>) {
flow.value = new.map { it.server }.toSet()
}
})
flow.value = accountManager.accounts.map { it.server }.toSet()
return flow
}
private fun createRemotesFlow(): StateFlow<Set<GitRemoteUrlCoordinates>> {
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<GitRemoteUrlCoordinates> {
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<Set<GithubServerPath>>,
remotesFlow: StateFlow<Collection<GitRemoteUrlCoordinates>>): StateFlow<Set<GithubServerPath>> {
val stateFlow = MutableStateFlow<Set<GithubServerPath>>(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<GithubServerPath>, discoveredServers: Set<GithubServerPath>): Set<GithubServerPath> {
val servers = mutableSetOf<GithubServerPath>(GithubServerPath.DEFAULT_SERVER)
servers.addAll(accountsServers)
servers.addAll(discoveredServers)
return servers
}
private fun collectMappings(servers: Set<GithubServerPath>, remotes: Collection<GitRemoteUrlCoordinates>): Set<GHGitRepositoryMapping> {
val mappings = HashSet<GHGitRepositoryMapping>()
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<GHGitRepositoryMapping> {
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<Set<GithubServerPath>>) {
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<GHHostedRepositoriesManager>()
private const val URLS_CHECK_PARALLELISM = 10
}
}
@@ -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<GHGitRepositoryMapping>()) { _, oldValue, newValue ->
if (oldValue != newValue) {
ApplicationManager.getApplication().messageBus.syncPublisher(LIST_CHANGES_TOPIC).repositoryListChanged(newValue, project)
}
}
private set
private val serversFromDiscovery = HashSet<GithubServerPath>()
val knownRepositories: Set<GHGitRepositoryMapping>
get() = repositoriesManager.knownRepositoriesState.value
init {
accountManager.addListener(this, object : AccountsListener<GithubAccount> {
override fun onAccountListChanged(old: Collection<GithubAccount>, new: Collection<GithubAccount>) = runInEdt {
updateRepositories()
}
})
updateRepositories()
}
fun findKnownRepositories(repository: GitRepository): List<GHGitRepositoryMapping> {
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<GitRepositoryManager>().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<GithubServerPath>().apply {
add(GithubServerPath.DEFAULT_SERVER)
addAll(authenticatedServers)
addAll(serversFromDiscovery)
}
val repositories = HashSet<GHGitRepositoryMapping>()
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<GHEnterpriseServerMetadataLoader>().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<GHEnterpriseServerMetadataLoader>()
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<GHGitRepositoryMapping>, 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<GHGitRepositoryMapping>, project: Project)
}
companion object {
private val LOG = logger<GHProjectRepositoriesManager>()
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<GHProjectRepositoriesManager>().updateRepositories()
}
}
catch (e: Exception) {
LOG.info("Error occurred while updating repositories", e)
}
}
}
override fun dispose() {}
}
@@ -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<GHProjectRepositoriesManager>().knownRepositories.any {
val mappings = project.service<GHHostedRepositoriesManager>().knownRepositories
assertTrue("GitHub remote is not configured, current mappings: $mappings", mappings.any {
it.gitRemoteUrlCoordinates.repository == repository
})
}