mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
[github] implement a more modern changes browser for PR creation view
GitOrigin-RevId: 99cb0f37597ff158a0d3b4579a1fbd90fcbd6db4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4fd57d4e4a
commit
5f6f276e8c
@@ -1,13 +1,16 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.ui.codereview.details.model
|
||||
|
||||
import com.intellij.collaboration.ui.SimpleEventListener
|
||||
import com.intellij.collaboration.ui.codereview.details.model.CodeReviewChangeListViewModel.SelectionRequest
|
||||
import com.intellij.collaboration.util.ChangesSelection
|
||||
import com.intellij.collaboration.util.RefComparisonChange
|
||||
import com.intellij.openapi.actionSystem.DataKey
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.util.EventDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -85,6 +88,7 @@ abstract class CodeReviewChangeListViewModelBase(
|
||||
|
||||
private val _changesSelection = MutableStateFlow<ChangesSelection?>(null)
|
||||
override val changesSelection: StateFlow<ChangesSelection?> = _changesSelection.asStateFlow()
|
||||
private val selectionMulticaster = EventDispatcher.create(SimpleEventListener::class.java)
|
||||
|
||||
protected val selectedCommit: String? = changeList.commitSha
|
||||
|
||||
@@ -99,6 +103,7 @@ abstract class CodeReviewChangeListViewModelBase(
|
||||
_selectionRequests.tryEmit(SelectionRequest.All)
|
||||
}
|
||||
else {
|
||||
if (!changeList.changes.contains(change)) return
|
||||
val currentSelection = _changesSelection.value
|
||||
if (currentSelection == null || currentSelection !is ChangesSelection.Fuzzy || !currentSelection.changes.contains(change)) {
|
||||
_changesSelection.value = ChangesSelection.Precise(changeList.changes, change)
|
||||
@@ -116,9 +121,25 @@ abstract class CodeReviewChangeListViewModelBase(
|
||||
if (!stateGuard.tryLock()) return
|
||||
try {
|
||||
_changesSelection.value = selection
|
||||
selectionMulticaster.multicaster.eventOccurred()
|
||||
}
|
||||
finally {
|
||||
stateGuard.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener invoked SYNCHRONOUSLY when selection is changed
|
||||
*/
|
||||
suspend fun handleSelection(listener: (ChangesSelection?) -> Unit): Nothing {
|
||||
val simpleListener = SimpleEventListener { listener(changesSelection.value) }
|
||||
try {
|
||||
selectionMulticaster.addListener(simpleListener)
|
||||
listener(changesSelection.value)
|
||||
awaitCancellation()
|
||||
}
|
||||
finally {
|
||||
selectionMulticaster.removeListener(simpleListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.ui.codereview.details.model
|
||||
|
||||
import com.intellij.collaboration.async.mapState
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.updateAndGet
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Internal
|
||||
@ApiStatus.NonExtendable
|
||||
interface CodeReviewCommitsChangesStateHandler<C : Any, VM : Any> {
|
||||
val selectedCommit: StateFlow<C?>
|
||||
val changeListVm: StateFlow<VM>
|
||||
|
||||
fun selectCommit(index: Int): VM?
|
||||
fun selectCommit(commit: C?): VM?
|
||||
fun selectNextCommit(): VM?
|
||||
fun selectPreviousCommit(): VM?
|
||||
|
||||
companion object {
|
||||
@JvmOverloads
|
||||
fun <C : Any, VM : Any> create(
|
||||
cs: CoroutineScope,
|
||||
commits: List<C>,
|
||||
commitChangesVmProducer: CoroutineScope.(C?) -> VM,
|
||||
initialCommitIdx: Int = -1,
|
||||
): CodeReviewCommitsChangesStateHandler<C, VM> =
|
||||
CodeReviewCommitsChangesStateHandlerImpl(cs, commits, commitChangesVmProducer, initialCommitIdx)
|
||||
}
|
||||
}
|
||||
|
||||
private class CodeReviewCommitsChangesStateHandlerImpl<C : Any, VM : Any>(
|
||||
private val cs: CoroutineScope,
|
||||
private val commits: List<C>,
|
||||
private val commitChangesVmProducer: CoroutineScope.(C?) -> VM,
|
||||
initialCommitIdx: Int = -1,
|
||||
) : CodeReviewCommitsChangesStateHandler<C, VM> {
|
||||
private val state = MutableStateFlow(createState(initialCommitIdx))
|
||||
|
||||
override val selectedCommit: StateFlow<C?> = state.mapState { commits.getOrNull(it.commitIdx) }
|
||||
override val changeListVm: StateFlow<VM> = state.mapState { it.vm }
|
||||
|
||||
override fun selectCommit(index: Int): VM? {
|
||||
if (index > 0 && index !in commits.indices) return null
|
||||
return state.updateAndGet {
|
||||
it.changeCommit(index)
|
||||
}.vm
|
||||
}
|
||||
|
||||
override fun selectCommit(commit: C?): VM? {
|
||||
val idx = commits.indexOf(commit).takeIf { it >= 0 } ?: return null
|
||||
return state.updateAndGet {
|
||||
it.changeCommit(idx)
|
||||
}.vm
|
||||
}
|
||||
|
||||
override fun selectNextCommit(): VM? = state.updateAndGet {
|
||||
val newIdx = it.commitIdx + 1
|
||||
if (newIdx !in commits.indices) return null // return out of select, do not update
|
||||
it.changeCommit(newIdx)
|
||||
}.vm
|
||||
|
||||
override fun selectPreviousCommit(): VM? = state.updateAndGet {
|
||||
val newIdx = it.commitIdx - 1
|
||||
if (newIdx !in commits.indices) return null // return out of select, do not update
|
||||
it.changeCommit(newIdx)
|
||||
}.vm
|
||||
|
||||
private fun State.changeCommit(commitIdx: Int): State {
|
||||
if (this.commitIdx == commitIdx) return this
|
||||
vmCs.cancel()
|
||||
return createState(commitIdx)
|
||||
}
|
||||
|
||||
private fun createState(commitIdx: Int): State {
|
||||
val newCs = cs.childScope("Commit Changes View Model")
|
||||
val commit = commits.getOrNull(commitIdx)
|
||||
return State(commitIdx, newCs, newCs.commitChangesVmProducer(commit))
|
||||
}
|
||||
|
||||
private inner class State(
|
||||
val commitIdx: Int,
|
||||
val vmCs: CoroutineScope,
|
||||
val vm: VM
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,11 @@ class CodeReviewDiffViewModelComputer<D> @ApiStatus.Experimental constructor(
|
||||
changesRequests.emit(selection)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun tryShowChanges(selection: ChangesSelection) {
|
||||
changesRequests.tryEmit(selection)
|
||||
}
|
||||
|
||||
val diffVm: Flow<ComputedResult<DiffProducersViewModel?>> =
|
||||
dataLoadingFlow.mapNotNull { it.result }.distinctUntilChanged().mapScoped { result ->
|
||||
result.fold(
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.ui.codereview.diff.model
|
||||
|
||||
import com.intellij.collaboration.ui.SimpleEventListener
|
||||
import com.intellij.collaboration.util.ComputedResult
|
||||
import com.intellij.openapi.vcs.changes.ui.ChangeDiffRequestChain
|
||||
import com.intellij.util.EventDispatcher
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -30,6 +34,8 @@ class DiffProducersViewModel private constructor(initialState: State) {
|
||||
private val _producers = MutableStateFlow(initialState)
|
||||
val producers: StateFlow<State> = _producers.asStateFlow()
|
||||
|
||||
private val selectionMulticaster = EventDispatcher.create(SimpleEventListener::class.java)
|
||||
|
||||
internal fun canNavigate(): Boolean = producers.value.producers.size > 1
|
||||
|
||||
internal fun canSelectPrev(): Boolean = producers.value.selectedIdx > 0
|
||||
@@ -46,6 +52,23 @@ class DiffProducersViewModel private constructor(initialState: State) {
|
||||
|
||||
internal fun updateProducers(newStateSupplier: (State) -> State) {
|
||||
_producers.update(newStateSupplier)
|
||||
selectionMulticaster.multicaster.eventOccurred()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener invoked SYNCHRONOUSLY when selection is changed
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun handleSelection(listener: (ChangeDiffRequestChain.Producer?) -> Unit): Nothing {
|
||||
val simpleListener = SimpleEventListener { listener(producers.value.getSelected()) }
|
||||
try {
|
||||
selectionMulticaster.addListener(simpleListener)
|
||||
listener(producers.value.getSelected())
|
||||
awaitCancellation()
|
||||
}
|
||||
finally {
|
||||
selectionMulticaster.removeListener(simpleListener)
|
||||
}
|
||||
}
|
||||
|
||||
data class State internal constructor(val producers: List<ChangeDiffRequestChain.Producer>, val selectedIdx: Int) {
|
||||
|
||||
@@ -488,6 +488,10 @@ public final class GitChangeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getThreeDotDiffOrThrow
|
||||
*/
|
||||
@Deprecated
|
||||
public static @NotNull Collection<Change> getThreeDotDiff(@NotNull GitRepository repository,
|
||||
@NotNull @NonNls String oldRevision,
|
||||
@NotNull @NonNls String newRevision) {
|
||||
@@ -500,6 +504,12 @@ public final class GitChangeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull Collection<Change> getThreeDotDiffOrThrow(@NotNull GitRepository repository,
|
||||
@NotNull @NonNls String oldRevision,
|
||||
@NotNull @NonNls String newRevision) throws VcsException {
|
||||
return getDiff(repository.getProject(), repository.getRoot(), oldRevision, newRevision, null, true, true);
|
||||
}
|
||||
|
||||
public static class GitDiffChange implements FilePathChange {
|
||||
private final @NotNull FileStatus status;
|
||||
private final @Nullable FilePath beforePath;
|
||||
|
||||
@@ -155,9 +155,10 @@ pull.request.create.no.changes=No changes between ''{0}'' and ''{1}''
|
||||
pull.request.create.error=Failed to create a pull request
|
||||
pull.request.create.already.exists=Pull request already exists
|
||||
pull.request.create.already.exists.view=View
|
||||
pull.request.create.select.branches=Selected pull request base and head to view changes
|
||||
pull.request.create.select.branches=Select pull request base and head to view changes
|
||||
pull.request.create.no.commits=No commits
|
||||
pull.request.create.failed.to.load.commits=Failed to load commits
|
||||
pull.request.create.failed.to.load.changes=Failed to load changes
|
||||
pull.request.create.checking.branches=Checking pull request branches
|
||||
pull.request.create.failed.to.check.branches=Failed to check pull request branches
|
||||
pull.request.create.pushing=Pushing a pull request branch
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
package org.jetbrains.plugins.github.pullrequest
|
||||
|
||||
import com.intellij.collaboration.file.codereview.CodeReviewDiffVirtualFile
|
||||
import com.intellij.collaboration.ui.codereview.CodeReviewAdvancedSettings
|
||||
import com.intellij.diff.impl.DiffEditorViewer
|
||||
import com.intellij.diff.util.DiffUserDataKeysEx
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.vcs.editor.ComplexPathVirtualFileSystem
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
|
||||
import org.jetbrains.plugins.github.i18n.GithubBundle
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContextRepository
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.diff.GHPRDiffService
|
||||
|
||||
internal data class GHNewPRDiffVirtualFile(private val fileManagerId: String,
|
||||
private val project: Project,
|
||||
@@ -23,7 +27,14 @@ internal data class GHNewPRDiffVirtualFile(private val fileManagerId: String,
|
||||
override fun isValid(): Boolean = isFileValid(fileManagerId, project, repository)
|
||||
|
||||
override fun createViewer(project: Project): DiffEditorViewer {
|
||||
TODO("Not implemented yet")
|
||||
val processor = if (CodeReviewAdvancedSettings.isCombinedDiffEnabled()) {
|
||||
project.service<GHPRDiffService>().createCombinedDiffProcessor(repository)
|
||||
}
|
||||
else {
|
||||
project.service<GHPRDiffService>().createDiffRequestProcessor(repository)
|
||||
}
|
||||
processor.context.putUserData(DiffUserDataKeysEx.COMBINED_DIFF_TOGGLE, CodeReviewAdvancedSettings.CodeReviewCombinedDiffToggle)
|
||||
return processor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ internal interface GHPRFilesManager : Disposable {
|
||||
fun createAndOpenTimelineFile(pullRequest: GHPRIdentifier, requestFocus: Boolean)
|
||||
|
||||
@RequiresEdt
|
||||
fun createAndOpenDiffFile(pullRequest: GHPRIdentifier, requestFocus: Boolean)
|
||||
fun createAndOpenDiffFile(pullRequest: GHPRIdentifier?, requestFocus: Boolean)
|
||||
|
||||
@RequiresEdt
|
||||
fun findTimelineFile(pullRequest: GHPRIdentifier): GHPRTimelineVirtualFile?
|
||||
@@ -27,4 +27,6 @@ internal interface GHPRFilesManager : Disposable {
|
||||
|
||||
@RequiresEdt
|
||||
fun updateTimelineFilePresentation(details: GHPullRequestShort)
|
||||
|
||||
suspend fun closeNewPrFile()
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@ package org.jetbrains.plugins.github.pullrequest.data
|
||||
import com.intellij.collaboration.util.CodeReviewFilesUtil
|
||||
import com.intellij.diff.editor.DiffEditorTabFilesManager
|
||||
import com.intellij.diff.editor.DiffVirtualFileBase
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.components.serviceAsync
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import com.intellij.util.containers.addIfNotNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
|
||||
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestShort
|
||||
import org.jetbrains.plugins.github.pullrequest.GHNewPRDiffVirtualFile
|
||||
@@ -42,15 +47,22 @@ internal class GHPRFilesManagerImpl(private val project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAndOpenDiffFile(pullRequest: GHPRIdentifier, requestFocus: Boolean) {
|
||||
diffFiles.getOrPut(pullRequest) {
|
||||
GHPRDiffVirtualFile(id, project, repository, pullRequest)
|
||||
override fun createAndOpenDiffFile(pullRequest: GHPRIdentifier?, requestFocus: Boolean) {
|
||||
if (pullRequest == null) {
|
||||
createOrGetNewPRDiffFile()
|
||||
}
|
||||
else {
|
||||
diffFiles.getOrPut(pullRequest) {
|
||||
GHPRDiffVirtualFile(id, project, repository, pullRequest)
|
||||
}.also {
|
||||
GHPRStatisticsCollector.logDiffOpened(project)
|
||||
}
|
||||
}.let {
|
||||
DiffEditorTabFilesManager.getInstance(project).showDiffFile(it, requestFocus)
|
||||
GHPRStatisticsCollector.logDiffOpened(project)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun findTimelineFile(pullRequest: GHPRIdentifier): GHPRTimelineVirtualFile? = files[pullRequest]
|
||||
|
||||
override fun findDiffFile(pullRequest: GHPRIdentifier): DiffVirtualFileBase? = diffFiles[pullRequest]
|
||||
@@ -61,6 +73,16 @@ internal class GHPRFilesManagerImpl(private val project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun closeNewPrFile() {
|
||||
val file = newPRDiffFile.get() ?: return
|
||||
withContext(Dispatchers.EDT) {
|
||||
val fileManager = serviceAsync<FileEditorManager>()
|
||||
writeAction {
|
||||
CodeReviewFilesUtil.closeFilesSafely(fileManager, listOf(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (project.isDisposed) return
|
||||
val fileManager = FileEditorManager.getInstance(project)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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.data.service
|
||||
|
||||
import com.intellij.collaboration.util.RefComparisonChange
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitRemoteBranch
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryPath
|
||||
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestShort
|
||||
@@ -18,4 +21,8 @@ interface GHPRCreationService {
|
||||
suspend fun findOpenPullRequest(baseBranch: GitRemoteBranch?,
|
||||
headRepo: GHRepositoryPath,
|
||||
headBranch: GitRemoteBranch): GHPRIdentifier?
|
||||
|
||||
suspend fun getDiff(commit: VcsCommitMetadata): Collection<RefComparisonChange>
|
||||
|
||||
suspend fun getDiff(baseBranch: GitRemoteBranch, headBranch: GitBranch): Collection<RefComparisonChange>
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
// 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.data.service
|
||||
|
||||
import com.intellij.collaboration.util.RefComparisonChange
|
||||
import com.intellij.openapi.progress.coroutineToIndicator
|
||||
import com.intellij.openapi.vcs.changes.Change
|
||||
import com.intellij.openapi.vcs.history.ShortVcsRevisionNumber
|
||||
import com.intellij.util.asSafely
|
||||
import com.intellij.util.text.nullize
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitRemoteBranch
|
||||
import git4idea.GitRevisionNumber
|
||||
import git4idea.changes.GitChangeUtils
|
||||
import git4idea.history.GitCommitRequirements
|
||||
import git4idea.history.GitHistoryUtils
|
||||
import git4idea.history.GitLogUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.plugins.github.api.*
|
||||
import org.jetbrains.plugins.github.api.data.GithubIssueState
|
||||
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestShort
|
||||
@@ -46,4 +60,42 @@ internal class GHPRCreationServiceImpl(private val requestExecutor: GithubApiReq
|
||||
|
||||
private fun getHeadRepoPrefix(headRepo: GHGitRepositoryMapping) =
|
||||
if (baseRepo.repository == headRepo.repository) "" else headRepo.repository.repositoryPath.owner + ":"
|
||||
|
||||
override suspend fun getDiff(commit: VcsCommitMetadata): Collection<RefComparisonChange> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
coroutineToIndicator {
|
||||
buildList {
|
||||
GitLogUtil.readFullDetailsForHashes(baseRepo.gitRepository.project, baseRepo.gitRepository.root,
|
||||
listOf(commit.id.asString()),
|
||||
GitCommitRequirements.DEFAULT) { gitCommit ->
|
||||
gitCommit.changes.forEach { change ->
|
||||
add(change.toComparisonChange(commit.parents.first().asString(), commit.id.asString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDiff(baseBranch: GitRemoteBranch, headBranch: GitBranch): Collection<RefComparisonChange> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
coroutineToIndicator {
|
||||
val mergeBase = GitHistoryUtils.getMergeBase(baseRepo.gitRepository.project, baseRepo.gitRepository.root,
|
||||
baseBranch.name, headBranch.name)
|
||||
?: error("Unrelated branches ${baseBranch.name} ${headBranch.name}")
|
||||
val headRef = baseRepo.gitRepository.branches.getHash(headBranch) ?: error("Branch ${headBranch.name} has bo revision")
|
||||
GitChangeUtils.getThreeDotDiffOrThrow(baseRepo.gitRepository, baseBranch.name, headBranch.name).map {
|
||||
it.toComparisonChange(mergeBase.rev, headRef.asString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Change.toComparisonChange(commitBefore: String, commitAfter: String): RefComparisonChange {
|
||||
val beforeRef = beforeRevision?.revisionNumber?.asSafely<ShortVcsRevisionNumber>() ?: GitRevisionNumber(commitBefore)
|
||||
val afterRef = afterRevision?.revisionNumber?.asSafely<ShortVcsRevisionNumber>() ?: GitRevisionNumber(commitAfter)
|
||||
return RefComparisonChange(
|
||||
beforeRef, beforeRevision?.file, afterRef, afterRevision?.file
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package org.jetbrains.plugins.github.pullrequest.ui.diff
|
||||
import com.intellij.collaboration.messages.CollaborationToolsBundle
|
||||
import com.intellij.collaboration.ui.codereview.action.ImmutableToolbarLabelAction
|
||||
import com.intellij.collaboration.ui.codereview.diff.CodeReviewDiffHandlerHelper
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.ComputedDiffViewModel
|
||||
import com.intellij.collaboration.util.KeyValuePair
|
||||
import com.intellij.diff.impl.DiffRequestProcessor
|
||||
import com.intellij.diff.tools.combined.CombinedDiffComponentProcessor
|
||||
@@ -17,10 +18,7 @@ import com.intellij.openapi.util.Disposer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
|
||||
import org.jetbrains.plugins.github.pullrequest.comment.action.GHPRDiffReviewThreadsReloadAction
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
|
||||
@@ -53,6 +51,16 @@ internal class GHPRDiffService(private val project: Project, parentCs: Coroutine
|
||||
GHPRDiffReviewThreadsReloadAction(),
|
||||
ActionManager.getInstance().getAction("Github.PullRequest.Review.Submit"))))
|
||||
}
|
||||
|
||||
fun createDiffRequestProcessor(repository: GHRepositoryCoordinates): DiffRequestProcessor {
|
||||
val vm = findDiffVm(project, repository)
|
||||
return base.createDiffRequestProcessor(vm) { emptyList() }
|
||||
}
|
||||
|
||||
fun createCombinedDiffProcessor(repository: GHRepositoryCoordinates): CombinedDiffComponentProcessor {
|
||||
val vm = findDiffVm(project, repository)
|
||||
return base.createCombinedDiffModel(vm) { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -66,6 +74,14 @@ private fun findDiffVm(project: Project, repository: GHRepositoryCoordinates, pu
|
||||
}
|
||||
} ?: flowOf(null)
|
||||
|
||||
private fun findDiffVm(project: Project, repository: GHRepositoryCoordinates): Flow<ComputedDiffViewModel?> =
|
||||
project.serviceIfCreated<GHPRToolWindowViewModel>()?.projectVm?.map {
|
||||
if (it?.repository == repository) {
|
||||
it.getCreateVmOrNull()?.diffVm
|
||||
}
|
||||
else null
|
||||
} ?: flowOf(null)
|
||||
|
||||
private fun GHPRToolWindowProjectViewModel.getDiffViewModelFlow(pullRequest: GHPRIdentifier): Flow<GHPRDiffViewModel> = channelFlow {
|
||||
val acquisitionDisposable = Disposer.newDisposable()
|
||||
val vm = acquireDiffViewModel(pullRequest, acquisitionDisposable)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2000-2024 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.create
|
||||
|
||||
import com.intellij.collaboration.async.*
|
||||
import com.intellij.collaboration.ui.codereview.details.model.*
|
||||
import com.intellij.collaboration.util.ComputedResult
|
||||
import com.intellij.collaboration.util.RefComparisonChange
|
||||
import com.intellij.collaboration.util.map
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitRemoteBranch
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProjectUISettings
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContext
|
||||
|
||||
internal class GHPRCreateChangesViewModel(
|
||||
private val project: Project,
|
||||
private val settings: GithubPullRequestsProjectUISettings,
|
||||
parentCs: CoroutineScope,
|
||||
private val dataContext: GHPRDataContext,
|
||||
private val baseBranch: GitRemoteBranch,
|
||||
private val headBranch: GitBranch,
|
||||
private val commits: List<VcsCommitMetadata>,
|
||||
) : CodeReviewChangesViewModel<VcsCommitMetadata> {
|
||||
private val cs = parentCs.childScope(javaClass.name)
|
||||
|
||||
override val reviewCommits: StateFlow<List<VcsCommitMetadata>> = MutableStateFlow(commits)
|
||||
|
||||
private val stateHandler = CodeReviewCommitsChangesStateHandler.create(cs, commits, {
|
||||
CommitChangesViewModel(this, it).apply {
|
||||
selectChange(null)
|
||||
}
|
||||
})
|
||||
|
||||
override val selectedCommitIndex: StateFlow<Int> = stateHandler.selectedCommit.mapState { commits.indexOf(it) }
|
||||
override val selectedCommit: StateFlow<VcsCommitMetadata?> = stateHandler.selectedCommit
|
||||
|
||||
val commitChangesVm: StateFlow<CommitChangesViewModel> = stateHandler.changeListVm
|
||||
|
||||
override fun selectCommit(index: Int) {
|
||||
stateHandler.selectCommit(index)?.selectChange(null)
|
||||
}
|
||||
|
||||
override fun selectNextCommit() {
|
||||
stateHandler.selectNextCommit()?.selectChange(null)
|
||||
}
|
||||
|
||||
override fun selectPreviousCommit() {
|
||||
stateHandler.selectPreviousCommit()?.selectChange(null)
|
||||
}
|
||||
|
||||
override fun commitHash(commit: VcsCommitMetadata): String = commit.id.toShortString()
|
||||
|
||||
inner class CommitChangesViewModel(cs: CoroutineScope, commit: VcsCommitMetadata?) {
|
||||
private val changeSelectionRequests = MutableSharedFlow<RefComparisonChange?>(replay = 1)
|
||||
|
||||
val changeListVm: StateFlow<ComputedResult<CodeReviewChangeListViewModelBase>> = computationStateFlow(flowOf(Unit)) {
|
||||
loadChanges(commit)
|
||||
}.mapScoped { result ->
|
||||
result.map {
|
||||
createChangesVm(commit, it)
|
||||
}
|
||||
}.stateInNow(cs, ComputedResult.loading())
|
||||
|
||||
private fun CoroutineScope.createChangesVm(commit: VcsCommitMetadata?, changes: Collection<RefComparisonChange>) =
|
||||
GHPRCreateChangeListViewModel(this, commit, changes.toList()).also { vm ->
|
||||
launchNow {
|
||||
changeSelectionRequests.collect {
|
||||
vm.selectChange(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadChanges(commit: VcsCommitMetadata?): Collection<RefComparisonChange> =
|
||||
if (commit == null) {
|
||||
dataContext.creationService.getDiff(baseBranch, headBranch)
|
||||
}
|
||||
else {
|
||||
dataContext.creationService.getDiff(commit)
|
||||
}
|
||||
|
||||
fun selectChange(change: RefComparisonChange?) {
|
||||
changeSelectionRequests.tryEmit(change)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GHPRCreateChangeListViewModel(parentCs: CoroutineScope, commit: VcsCommitMetadata?, changes: List<RefComparisonChange>)
|
||||
: CodeReviewChangeListViewModelBase(parentCs, CodeReviewChangeList(commit?.id?.asString(), changes)),
|
||||
CodeReviewChangeListViewModel.WithGrouping {
|
||||
override val grouping: StateFlow<Set<String>> = settings.changesGroupingState
|
||||
|
||||
override fun setGrouping(grouping: Collection<String>) {
|
||||
settings.changesGrouping = grouping.toSet()
|
||||
}
|
||||
|
||||
override val project: Project = this@GHPRCreateChangesViewModel.project
|
||||
|
||||
override fun showDiffPreview() {
|
||||
dataContext.filesManager.createAndOpenDiffFile(null, true)
|
||||
}
|
||||
|
||||
override fun showDiff() {
|
||||
showDiffPreview() // implement
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,29 @@ import com.intellij.collaboration.async.collectScoped
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.ui.*
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil.defaultButton
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil.moveToCenter
|
||||
import com.intellij.collaboration.ui.codereview.avatar.Avatar
|
||||
import com.intellij.collaboration.ui.codereview.commits.CommitsBrowserComponentBuilder
|
||||
import com.intellij.collaboration.ui.codereview.changes.CodeReviewChangeListComponentFactory
|
||||
import com.intellij.collaboration.ui.codereview.create.CodeReviewCreateReviewUIUtil
|
||||
import com.intellij.collaboration.ui.codereview.details.CodeReviewDetailsCommitInfoComponentFactory
|
||||
import com.intellij.collaboration.ui.codereview.details.CodeReviewDetailsCommitsComponentFactory
|
||||
import com.intellij.collaboration.ui.codereview.details.CommitPresentation
|
||||
import com.intellij.collaboration.ui.codereview.details.model.CodeReviewChangeListViewModel
|
||||
import com.intellij.collaboration.ui.codereview.list.error.ErrorStatusPanelFactory
|
||||
import com.intellij.collaboration.ui.codereview.list.error.ErrorStatusPresenter
|
||||
import com.intellij.collaboration.ui.layout.SizeRestrictedSingleComponentLayout
|
||||
import com.intellij.collaboration.ui.util.*
|
||||
import com.intellij.collaboration.util.*
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
import com.intellij.openapi.actionSystem.EdtNoGetDataProvider
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.messages.MessagesService
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.toolWindow.InternalDecoratorImpl
|
||||
@@ -29,6 +38,7 @@ import com.intellij.ui.components.JBOptionButton
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.util.ui.*
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import com.intellij.vcs.log.util.VcsUserUtil
|
||||
import com.intellij.vcsUtil.showAbove
|
||||
import git4idea.ui.branch.MergeDirectionComponentFactory
|
||||
import kotlinx.coroutines.*
|
||||
@@ -57,6 +67,7 @@ import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ContainerEvent
|
||||
import java.awt.event.ContainerListener
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
@@ -81,7 +92,7 @@ internal object GHPRCreateComponentFactory {
|
||||
layout = MigLayout(LC().gridGap("0", "0").insets("0").flowY().fill())
|
||||
|
||||
add(directionSelector(vm), CC().pushX().gap(SIDE_GAPS_L, SIDE_GAPS_L, SIDE_GAPS_M, SIDE_GAPS_M))
|
||||
add(commitsPanel(vm).apply {
|
||||
add(changesPanel(vm).apply {
|
||||
border = IdeBorderFactory.createBorder(SideBorder.TOP)
|
||||
}, CC().grow().push())
|
||||
}
|
||||
@@ -222,34 +233,76 @@ internal object GHPRCreateComponentFactory {
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun CoroutineScope.commitsPanel(vm: GHPRCreateViewModel): JComponent {
|
||||
private fun CoroutineScope.changesPanel(vm: GHPRCreateViewModel): JComponent {
|
||||
val wrapper = Wrapper()
|
||||
val errorStatusPresenter = ErrorStatusPresenter.simple(message("pull.request.create.failed.to.load.commits"),
|
||||
descriptionProvider = GHHtmlErrorPanel::getLoadingErrorText)
|
||||
launchNow {
|
||||
vm.commits.debounce(50).collect { commitsResult ->
|
||||
val content = commitsResult?.fold(::LoadingTextLabel,
|
||||
{ createCommitsPanel(vm.project, it) },
|
||||
{
|
||||
val errorStatusPresenter = ErrorStatusPresenter.simple(message("pull.request.create.failed.to.load.commits"),
|
||||
descriptionProvider = GHHtmlErrorPanel::getLoadingErrorText)
|
||||
ErrorStatusPanelFactory.create(it, errorStatusPresenter)
|
||||
})
|
||||
?: SimpleHtmlPane(message("pull.request.create.select.branches"))
|
||||
vm.changesVm.debounce(50).collectScoped { changesResult ->
|
||||
val content = changesResult?.fold({ moveToCenter(LoadingTextLabel()) },
|
||||
{ createChangesPanel(it) },
|
||||
{ moveToCenter(ErrorStatusPanelFactory.create(it, errorStatusPresenter)) })
|
||||
?: SimpleHtmlPane(message("pull.request.create.select.branches")).let(CollaborationToolsUIUtil::moveToCenter)
|
||||
wrapper.setContent(content)
|
||||
}
|
||||
}
|
||||
return wrapper
|
||||
}
|
||||
|
||||
private fun createCommitsPanel(project: Project, commits: List<VcsCommitMetadata>): JComponent {
|
||||
val commitsModel = SingleValueModel(commits)
|
||||
val commitsPanel = CommitsBrowserComponentBuilder(project, commitsModel)
|
||||
.setCustomCommitRenderer(CodeReviewCreateReviewUIUtil.createCommitListCellRenderer())
|
||||
.showCommitDetails(true)
|
||||
.setEmptyCommitListText(message("pull.request.create.no.commits"))
|
||||
.create()
|
||||
return commitsPanel
|
||||
private fun CoroutineScope.createChangesPanel(changesVm: GHPRCreateChangesViewModel?): JComponent {
|
||||
if (changesVm == null) {
|
||||
return HintPane(message("pull.request.does.not.contain.commits")).let(CollaborationToolsUIUtil::moveToCenter)
|
||||
}
|
||||
|
||||
val cs = this
|
||||
val commits = CodeReviewDetailsCommitsComponentFactory
|
||||
.create(cs, changesVm, ::createCommitsPopupPresenter)
|
||||
val commitsDetails = CodeReviewDetailsCommitInfoComponentFactory
|
||||
.create(cs, changesVm.selectedCommit, ::createCommitsPopupPresenter, ::SimpleHtmlPane)
|
||||
|
||||
val commitsPanel = VerticalListPanel(SIDE_GAPS_M).apply {
|
||||
add(commits)
|
||||
add(commitsDetails)
|
||||
border = JBUI.Borders.empty(SIDE_GAPS_M, SIDE_GAPS_L)
|
||||
}
|
||||
|
||||
val scrollPane = ScrollPaneFactory.createScrollPane(null, true).apply {
|
||||
horizontalScrollBarPolicy = ScrollPaneFactory.HORIZONTAL_SCROLLBAR_NEVER
|
||||
}
|
||||
val panel = JPanel(BorderLayout()).apply {
|
||||
add(commitsPanel, BorderLayout.NORTH)
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
}.also {
|
||||
DataManager.registerDataProvider(it, object : EdtNoGetDataProvider {
|
||||
override fun dataSnapshot(sink: DataSink) {
|
||||
sink[CodeReviewChangeListViewModel.DATA_KEY] = changesVm.commitChangesVm.value.changeListVm.value.getOrNull()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val errorStatusPresenter = ErrorStatusPresenter.simple(message("pull.request.create.failed.to.load.changes"),
|
||||
descriptionProvider = GHHtmlErrorPanel::getLoadingErrorText)
|
||||
cs.launchNow {
|
||||
changesVm.commitChangesVm.collectScoped { commitChangesVm ->
|
||||
commitChangesVm.changeListVm.debounce(50).collectScoped { changeListVmResult ->
|
||||
val content = changeListVmResult.fold({ moveToCenter(LoadingTextLabel()) },
|
||||
{ createChangesPanel(it) },
|
||||
{ moveToCenter(ErrorStatusPanelFactory.create(it, errorStatusPresenter)) })
|
||||
scrollPane.setViewportView(content)
|
||||
panel.revalidate()
|
||||
panel.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun CoroutineScope.createChangesPanel(changeListVm: CodeReviewChangeListViewModel): JComponent =
|
||||
CodeReviewChangeListComponentFactory.createIn(this, changeListVm, null,
|
||||
message("pull.request.commit.does.not.contain.changes")).also {
|
||||
it.installPopupHandler(ActionManager.getInstance().getAction("Github.PullRequest.Changes.Popup") as ActionGroup)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.metadataPanel(vm: GHPRCreateViewModel): JComponent {
|
||||
val reviewersHandle = createReviewersListPanelHandle(vm.reviewersVm, vm.avatarIconsProvider)
|
||||
val assigneesHandle = createAssigneesListPanelHandle(vm.assigneesVm, vm.avatarIconsProvider)
|
||||
@@ -379,6 +432,13 @@ internal object GHPRCreateComponentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCommitsPopupPresenter(commit: VcsCommitMetadata) = CommitPresentation(
|
||||
titleHtml = commit.subject,
|
||||
descriptionHtml = commit.fullMessage.split("\n\n").getOrNull(1).orEmpty(),
|
||||
author = VcsUserUtil.getShortPresentation(commit.author),
|
||||
committedDate = Date(commit.authorTime)
|
||||
)
|
||||
|
||||
private fun CoroutineScope.createReviewersListPanelHandle(vm: LabeledListPanelViewModel<GHPullRequestRequestedReviewer>,
|
||||
avatarIconsProvider: GHAvatarIconsProvider) =
|
||||
LabeledListPanelHandle(this, vm,
|
||||
@@ -437,6 +497,11 @@ private fun ErrorLink(error: Throwable) =
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
private fun HintPane(message: @Nls String) = SimpleHtmlPane(message).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
}
|
||||
|
||||
private var Editor.margins
|
||||
get() = (this as? EditorEx)?.scrollPane?.viewportBorder?.getBorderInsets(scrollPane.viewport) ?: JBUI.emptyInsets()
|
||||
set(value) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2000-2024 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.create
|
||||
|
||||
import com.intellij.collaboration.ui.codereview.diff.CodeReviewDiffRequestProducer
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.CodeReviewDiffViewModelComputer
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.ComputedDiffViewModel
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.DiffProducersViewModel
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.RefComparisonChangesSorter
|
||||
import com.intellij.collaboration.ui.codereview.diff.viewer.buildChangeContext
|
||||
import com.intellij.collaboration.util.ChangesSelection
|
||||
import com.intellij.collaboration.util.ComputedResult
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import git4idea.changes.createVcsChange
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProjectUISettings
|
||||
|
||||
internal class GHPRCreateDiffViewModel(private val project: Project, parentCs: CoroutineScope) : ComputedDiffViewModel {
|
||||
private val cs = parentCs.childScope(javaClass.name)
|
||||
|
||||
private val changesSorter = GithubPullRequestsProjectUISettings.getInstance(project).changesGroupingState
|
||||
.map { RefComparisonChangesSorter.Grouping(project, it) }
|
||||
private val helper =
|
||||
CodeReviewDiffViewModelComputer(flowOf(ComputedResult.success(Unit)), changesSorter) { _, change ->
|
||||
val changeContext: Map<Key<*>, Any> = change.buildChangeContext()
|
||||
val changeDiffProducer = ChangeDiffRequestProducer.create(project, change.createVcsChange(project), changeContext)
|
||||
?: error("Could not create diff producer from $change")
|
||||
CodeReviewDiffRequestProducer(project, change, changeDiffProducer, null)
|
||||
}
|
||||
|
||||
override val diffVm: StateFlow<ComputedResult<DiffProducersViewModel?>> =
|
||||
helper.diffVm.stateIn(cs, SharingStarted.Eagerly, ComputedResult.loading())
|
||||
|
||||
fun showChanges(changes: ChangesSelection) {
|
||||
helper.tryShowChanges(changes)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@
|
||||
package org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create
|
||||
|
||||
import com.intellij.collaboration.async.*
|
||||
import com.intellij.collaboration.ui.codereview.diff.CodeReviewDiffRequestProducer
|
||||
import com.intellij.collaboration.ui.codereview.diff.model.ComputedDiffViewModel
|
||||
import com.intellij.collaboration.util.CollectionDelta
|
||||
import com.intellij.collaboration.util.ComputedResult
|
||||
import com.intellij.collaboration.util.computeEmitting
|
||||
import com.intellij.collaboration.util.getOrNull
|
||||
import com.intellij.dvcs.DvcsUtil
|
||||
import com.intellij.dvcs.push.PushSpec
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.serviceAsync
|
||||
import com.intellij.openapi.progress.coroutineToIndicator
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
@@ -55,7 +59,8 @@ internal interface GHPRCreateViewModel {
|
||||
val repositories: Collection<GHGitRepositoryMapping>
|
||||
val branches: StateFlow<BranchesState>
|
||||
|
||||
val commits: StateFlow<ComputedResult<List<VcsCommitMetadata>>?>
|
||||
val changesVm: StateFlow<ComputedResult<GHPRCreateChangesViewModel?>?>
|
||||
val diffVm: ComputedDiffViewModel
|
||||
|
||||
val titleText: StateFlow<String>
|
||||
val descriptionText: StateFlow<String>
|
||||
@@ -101,6 +106,7 @@ internal interface GHPRCreateViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class GHPRCreateViewModelImpl(override val project: Project,
|
||||
parentCs: CoroutineScope,
|
||||
private val repositoryManager: GHHostedRepositoriesManager,
|
||||
@@ -123,9 +129,33 @@ internal class GHPRCreateViewModelImpl(override val project: Project,
|
||||
override val branches: StateFlow<BranchesState> = branchesWithProgress.map { it.branches }
|
||||
.stateInNow(cs, branchesWithProgress.value.branches)
|
||||
|
||||
override val commits: StateFlow<ComputedResult<List<VcsCommitMetadata>>?> =
|
||||
branchesWithProgress.map { it.commitsRequest }.optionalComputationState()
|
||||
.stateIn(cs, SharingStarted.Lazily, ComputedResult.loading())
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val changesVm: StateFlow<ComputedResult<GHPRCreateChangesViewModel?>?> = branchesWithProgress.transformLatest { branches ->
|
||||
val commits = try {
|
||||
branches.commitsRequest?.await()
|
||||
}
|
||||
catch (ce: CancellationException) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
null
|
||||
}
|
||||
catch (e: Exception) {
|
||||
emit(ComputedResult.failure(e))
|
||||
return@transformLatest
|
||||
}
|
||||
if (commits == null) {
|
||||
emit(null)
|
||||
}
|
||||
else if (commits.isEmpty()) {
|
||||
emit(ComputedResult.success(null))
|
||||
}
|
||||
else coroutineScope {
|
||||
val settings = project.serviceAsync<GithubPullRequestsProjectUISettings>()
|
||||
val vm = GHPRCreateChangesViewModel(project, settings, this, dataContext,
|
||||
branches.branches.baseBranch!!, branches.branches.headBranch!!, commits)
|
||||
emit(ComputedResult.success(vm))
|
||||
}
|
||||
}.stateInNow(cs, null)
|
||||
override val diffVm = GHPRCreateDiffViewModel(project, cs)
|
||||
|
||||
override val titleText: MutableStateFlow<String> = MutableStateFlow("")
|
||||
override val descriptionText: MutableStateFlow<String> = MutableStateFlow("")
|
||||
@@ -177,17 +207,41 @@ internal class GHPRCreateViewModelImpl(override val project: Project,
|
||||
setTitleFromFirstCommitOrBranch(headBranch, commits)
|
||||
}
|
||||
}
|
||||
|
||||
cs.launchNow {
|
||||
changesVm.flatMapLatest {
|
||||
it?.getOrNull()?.commitChangesVm ?: flowOf(null)
|
||||
}.flatMapLatest {
|
||||
it?.changeListVm ?: flowOf(null)
|
||||
}.map {
|
||||
it?.getOrNull()
|
||||
}.collectScoped { vm ->
|
||||
vm?.handleSelection {
|
||||
if (it != null) {
|
||||
diffVm.showChanges(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cs.launchNow {
|
||||
diffVm.diffVm.collectScoped {
|
||||
it.getOrNull()?.handleSelection { producer ->
|
||||
val change = (producer as? CodeReviewDiffRequestProducer)?.change ?: return@handleSelection
|
||||
val changesVm = changesVm.value?.getOrNull()
|
||||
//TODO: handle different commit
|
||||
val commitChangesVm = changesVm?.commitChangesVm?.value
|
||||
val changeListVm = commitChangesVm?.changeListVm?.value?.getOrNull()
|
||||
changeListVm?.selectChange(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTitleFromFirstCommitOrBranch(headBranch: GitBranch, commits: List<VcsCommitMetadata>) {
|
||||
titleLock.withLock {
|
||||
val singleCommit = commits.singleOrNull()
|
||||
titleText.value = if (singleCommit != null) {
|
||||
singleCommit.fullMessage.split("\n\n").firstOrNull().orEmpty()
|
||||
}
|
||||
else {
|
||||
headBranch.name
|
||||
}
|
||||
titleText.value = singleCommit?.subject ?: headBranch.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,14 +497,4 @@ private class MetadataListViewModel<T>(cs: CoroutineScope, itemsLoader: suspend
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun <T> Flow<Deferred<T>?>.optionalComputationState(): Flow<ComputedResult<T>?> =
|
||||
transformLatest { request ->
|
||||
if (request == null) {
|
||||
emit(null)
|
||||
return@transformLatest
|
||||
}
|
||||
flowOf(request).computationState().collect(this)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.jetbrains.plugins.github.pullrequest.ui.editor.GHPRReviewInEditorView
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.review.GHPRBranchWidgetViewModel
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.timeline.GHPRTimelineViewModel
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.GHPRToolWindowTab
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRCreateViewModel
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRCreateViewModelImpl
|
||||
import org.jetbrains.plugins.github.ui.util.GHUIUtil
|
||||
import org.jetbrains.plugins.github.util.DisposalCountingHolder
|
||||
@@ -54,7 +55,7 @@ class GHPRToolWindowProjectViewModel internal constructor(
|
||||
private val twVm: GHPRToolWindowViewModel,
|
||||
connection: GHRepositoryConnection
|
||||
) : ReviewToolwindowProjectViewModel<GHPRToolWindowTab, GHPRToolWindowTabViewModel> {
|
||||
private val cs = parentCs.childScope()
|
||||
private val cs = parentCs.childScope(javaClass.name)
|
||||
|
||||
internal val dataContext: GHPRDataContext = connection.dataContext
|
||||
|
||||
@@ -68,6 +69,7 @@ class GHPRToolWindowProjectViewModel internal constructor(
|
||||
private val lazyCreateVm = SynchronizedClearableLazy {
|
||||
GHPRCreateViewModelImpl(project, cs, repoManager, GithubPullRequestsProjectUISettings.getInstance(project), connection.dataContext, this)
|
||||
}
|
||||
internal fun getCreateVmOrNull(): GHPRCreateViewModel? = lazyCreateVm.valueIfInitialized
|
||||
|
||||
private val pullRequestsVms = Caffeine.newBuilder().build<GHPRIdentifier, DisposalCountingHolder<GHPRViewModelContainer>> { id ->
|
||||
DisposalCountingHolder {
|
||||
@@ -89,6 +91,9 @@ class GHPRToolWindowProjectViewModel internal constructor(
|
||||
tabsHelper.close(tab)
|
||||
if (reset) {
|
||||
lazyCreateVm.drop()?.let(Disposer::dispose)
|
||||
cs.launch {
|
||||
dataContext.filesManager.closeNewPrFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user