mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[github] Refresh PR-on-current-branch state on push (IJPL-249925)
GitOrigin-RevId: 44c504a162a28b576bbd59e59a59de06f1571f5d
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4776cf09a5
commit
e6ce52e589
@@ -3952,7 +3952,6 @@ a:git4idea.remote.GitRepositoryHostingService
|
||||
git4idea.remote.InteractiveGitHttpAuthDataProvider
|
||||
- a:getAuthData(java.awt.Component):com.intellij.util.AuthData
|
||||
f:git4idea.remote.hosting.GitAsyncExtensionsKt
|
||||
- sf:currentRemoteBranchFlow(git4idea.repo.GitRepository):kotlinx.coroutines.flow.Flow
|
||||
- sf:discoverServers(kotlinx.coroutines.flow.Flow,kotlinx.coroutines.flow.Flow,I,kotlin.jvm.functions.Function2):kotlinx.coroutines.flow.Flow
|
||||
- bs:discoverServers$default(kotlinx.coroutines.flow.Flow,kotlinx.coroutines.flow.Flow,I,kotlin.jvm.functions.Function2,I,java.lang.Object):kotlinx.coroutines.flow.Flow
|
||||
- sf:gitRemotesStateIn(com.intellij.openapi.project.Project,kotlinx.coroutines.CoroutineScope,kotlinx.coroutines.flow.SharingStarted):kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -6,6 +6,8 @@ import git4idea.repo.GitRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface HostedGitRepositoriesManager<M : HostedGitRepositoryMapping> {
|
||||
val knownRepositoriesState: StateFlow<Set<M>>
|
||||
@@ -19,9 +21,24 @@ fun <M : HostedGitRepositoryMapping> HostedGitRepositoriesManager<M>.findKnownRe
|
||||
it.remote.repository == repository
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the hosted repository mapping and the remote branch tracked by the current local branch (the branch whose
|
||||
* pull/merge request the current branch is reviewed against), or `null` when the current branch tracks no hosted branch.
|
||||
*
|
||||
* Re-emits not only when the tracked branch changes, but also when its tip commit moves (e.g. after a push): review
|
||||
* features rely on this to re-resolve "which PR/MR corresponds to the current branch" without requiring a branch switch.
|
||||
* [GitRemoteBranch] equality ignores the commit it points to, so the tip hash is folded into the dedup key explicitly.
|
||||
*/
|
||||
fun <M : HostedGitRepositoryMapping> HostedGitRepositoriesManager<M>.findHostedRemoteBranchTrackedByCurrent(repository: GitRepository)
|
||||
: Flow<Pair<M, GitRemoteBranch>?> =
|
||||
knownRepositoriesState.combine(repository.currentRemoteBranchFlow()) { repositories, branch ->
|
||||
knownRepositoriesState.combine(
|
||||
repository.infoFlow()
|
||||
.map { info ->
|
||||
val branch = info.findFirstRemoteBranchTrackedByCurrent()
|
||||
branch to branch?.let { info.remoteBranchesWithHashes[it] }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
) { repositories, (branch, _) ->
|
||||
if (branch == null) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -89,11 +89,6 @@ private fun GitRepositoryManager.collectRemotes(): Set<GitRemoteUrlCoordinates>
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun GitRepository.currentRemoteBranchFlow(): Flow<GitRemoteBranch?> =
|
||||
infoFlow()
|
||||
.map { it.findFirstRemoteBranchTrackedByCurrent() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun GitRepoInfo.findFirstRemoteBranchTrackedByCurrent(): GitRemoteBranch? {
|
||||
val currentBranch = currentBranch ?: return null
|
||||
return branchTrackInfos.find { it.localBranch == currentBranch }?.remoteBranch
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.remote.hosting
|
||||
|
||||
import com.intellij.openapi.vcs.Executor.cd
|
||||
import com.intellij.testFramework.common.waitUntil
|
||||
import git4idea.GitRemoteBranch
|
||||
import git4idea.push.GitPushOperationBaseTest
|
||||
import git4idea.remote.GitRemoteUrlCoordinates
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.test.git
|
||||
import git4idea.test.makeCommit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mockito.Mockito.mock
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class CurrentReviewBranchFlowTest : GitPushOperationBaseTest() {
|
||||
|
||||
private lateinit var repository: GitRepository
|
||||
private lateinit var manager: HostedGitRepositoriesManager<HostedGitRepositoryMapping>
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
val trinity = setupRepositories(projectPath, "parent", "bro")
|
||||
repository = trinity.projectRepo
|
||||
cd(projectPath)
|
||||
refresh()
|
||||
updateRepositories()
|
||||
|
||||
// A single hosted mapping for the "origin" remote of the test repository.
|
||||
val origin = repository.remotes.single { it.name == "origin" }
|
||||
val mapping = object : HostedGitRepositoryMapping {
|
||||
override val repository: HostedRepositoryCoordinates = mock(HostedRepositoryCoordinates::class.java)
|
||||
override val remote: GitRemoteUrlCoordinates = GitRemoteUrlCoordinates(origin.firstUrl!!, origin, this@CurrentReviewBranchFlowTest.repository)
|
||||
}
|
||||
manager = object : HostedGitRepositoriesManager<HostedGitRepositoryMapping> {
|
||||
override val knownRepositoriesState = MutableStateFlow(setOf(mapping))
|
||||
}
|
||||
}
|
||||
|
||||
fun `test re-emits the review branch when the current branch tip moves on push`() = runBlocking {
|
||||
val emissions = CopyOnWriteArrayList<Pair<HostedGitRepositoryMapping, GitRemoteBranch>?>()
|
||||
val job = launch(Dispatchers.Default) {
|
||||
manager.findHostedRemoteBranchTrackedByCurrent(repository).collect { emissions.add(it) }
|
||||
}
|
||||
try {
|
||||
// Initial resolution: the current branch (master) tracks origin/master.
|
||||
waitUntil("initial review branch is resolved", timeout = 10.seconds) { emissions.isNotEmpty() }
|
||||
assertEquals("master", emissions.last()?.second?.nameForRemoteOperations)
|
||||
val countBeforePush = emissions.size
|
||||
|
||||
// A push advances origin/master's tip while the tracked branch identity stays the same.
|
||||
makeCommit("file.txt")
|
||||
repository.git("push origin master")
|
||||
updateRepositories()
|
||||
|
||||
// The moved tip must produce a fresh emission (this is what the fix restores).
|
||||
waitUntil("review branch re-emitted after push", timeout = 10.seconds) { emissions.size > countBeforePush }
|
||||
assertEquals("master", emissions.last()?.second?.nameForRemoteOperations)
|
||||
}
|
||||
finally {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -12,8 +12,10 @@ import com.intellij.collaboration.util.SingleCoroutineLauncher
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.jetbrains.plugins.github.api.GHGQLRequests
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
|
||||
@@ -42,6 +44,10 @@ internal class GHPRListLoader(
|
||||
private val refreshRequests = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val updateRequests = MutableSharedFlow<Updated<GHPullRequestShort>>(replay = 1)
|
||||
|
||||
private val listUpdatedRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val listUpdated: Flow<Unit> = listUpdatedRequests.asSharedFlow()
|
||||
|
||||
private val loader = GraphQLListLoader.startIn(
|
||||
cs,
|
||||
{ it.id },
|
||||
@@ -76,12 +82,14 @@ internal class GHPRListLoader(
|
||||
|
||||
fun refresh() {
|
||||
refreshRequests.tryEmit(Unit)
|
||||
listUpdatedRequests.tryEmit(Unit)
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
cs.launchNow {
|
||||
reloadRequests.emit(Unit)
|
||||
}
|
||||
listUpdatedRequests.tryEmit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -118,9 +119,13 @@ abstract class GHPRConnectedProjectViewModelBase(
|
||||
private val prOnCurrentBranchRefreshSignal =
|
||||
MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
// Re-run the lookup on an explicit refresh and whenever the PR list is reloaded/refreshed, so a PR opened outside
|
||||
// the IDE (or otherwise missed by the initial lookup) is picked up on the next refresh without switching branches.
|
||||
private val prOnCurrentBranchRefresh = merge(prOnCurrentBranchRefreshSignal, dataContext.listLoader.listUpdated)
|
||||
|
||||
override val prOnCurrentBranch: StateFlow<ComputedResult<GHPRIdentifier?>?> =
|
||||
repoManager.findHostedRemoteBranchTrackedByCurrent(connection.repo.gitRepository)
|
||||
.combineTransform(prOnCurrentBranchRefreshSignal.withInitial(Unit)) { projectAndBranch, _ ->
|
||||
.combineTransform(prOnCurrentBranchRefresh.withInitial(Unit)) { projectAndBranch, _ ->
|
||||
if (projectAndBranch == null) {
|
||||
emit(ComputedResult.success(null))
|
||||
}
|
||||
|
||||
+6
-1
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -126,9 +127,13 @@ abstract class GitLabConnectedProjectViewModelBase(
|
||||
|
||||
protected val mergeRequestCreatedSignal: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
|
||||
// Re-run the lookup on MR creation and whenever the MR list is reloaded/refreshed, so an MR opened outside the IDE
|
||||
// (or otherwise missed by the initial lookup) is picked up on the next refresh without switching branches.
|
||||
private val mergeRequestOnCurrentBranchRefresh = merge(mergeRequestCreatedSignal, listVm.listUpdated)
|
||||
|
||||
private val mergeRequestOnCurrentBranch: Flow<String?> =
|
||||
projectsManager.findHostedRemoteBranchTrackedByCurrent(connection.repo.gitRepository)
|
||||
.combine(mergeRequestCreatedSignal.withInitial(Unit)) { repoAndBranch, _ ->
|
||||
.combine(mergeRequestOnCurrentBranchRefresh.withInitial(Unit)) { repoAndBranch, _ ->
|
||||
val (targetRepo, branch) = repoAndBranch ?: return@combine null
|
||||
try {
|
||||
findOpenReviewIdByBranch(connection, branch.nameForRemoteOperations, targetRepo.repository.projectPath.fullPath())
|
||||
|
||||
+14
@@ -12,8 +12,11 @@ import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
@@ -40,6 +43,12 @@ interface GitLabMergeRequestsListViewModel : ReviewListViewModel {
|
||||
val loading: Flow<Boolean>
|
||||
val error: Flow<Throwable?>
|
||||
|
||||
/**
|
||||
* Emits whenever the list is reloaded or refreshed. Consumers can use this to re-run branch-scoped lookups
|
||||
* (e.g. "which MR corresponds to the current branch") when the user refreshes the merge request list.
|
||||
*/
|
||||
val listUpdated: Flow<Unit>
|
||||
|
||||
fun requestMore()
|
||||
}
|
||||
|
||||
@@ -56,6 +65,9 @@ internal class GitLabMergeRequestsListViewModelImpl(
|
||||
private val scope = parentCs.childScope("GL MR List VM")
|
||||
private val requestMoreLauncher = SingleCoroutineLauncher(scope.childScope("Request More"))
|
||||
|
||||
private val listUpdatedSignal = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
override val listUpdated: Flow<Unit> = listUpdatedSignal.asSharedFlow()
|
||||
|
||||
private val loaderFlow: Flow<ReloadablePotentiallyInfiniteListLoader<GitLabMergeRequestShortRestDTO>> =
|
||||
filterVm.searchState
|
||||
.combine(tokenRefreshFlow.withInitial(Unit)) { search, _ -> search }
|
||||
@@ -75,12 +87,14 @@ internal class GitLabMergeRequestsListViewModelImpl(
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
listUpdatedSignal.tryEmit(Unit)
|
||||
scope.launch {
|
||||
loaderFlow.first().refresh()
|
||||
}
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
listUpdatedSignal.tryEmit(Unit)
|
||||
scope.launch {
|
||||
loaderFlow.first().reload()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user