From 558e66f0b0b5c43d876ea825084143b30faf5d6f Mon Sep 17 00:00:00 2001 From: Aleksandr Krasilnikov Date: Thu, 21 Jan 2021 12:41:53 +0300 Subject: [PATCH] [space] Add ability to checkout MR branch GitOrigin-RevId: 342cd559de34cd7796b155984d0248d93e43150a --- .../SpaceReviewCheckoutBranchAction.kt | 180 ++++++++++++++++++ .../vcs/review/details/SpaceReviewDetails.kt | 5 +- .../review/details/SpaceReviewDetailsVm.kt | 89 ++++++++- .../details/SpaceReviewInfoPanelFactory.kt | 31 ++- .../src/main/resources/META-INF/plugin.xml | 8 +- .../resources/messages/SpaceBundle.properties | 10 +- 6 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewCheckoutBranchAction.kt diff --git a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewCheckoutBranchAction.kt b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewCheckoutBranchAction.kt new file mode 100644 index 000000000000..fd4dcb6ab203 --- /dev/null +++ b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewCheckoutBranchAction.kt @@ -0,0 +1,180 @@ +// 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 com.intellij.space.vcs.review.details + +import circlet.code.api.MergeRequestBranch +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.space.messages.SpaceBundle +import com.intellij.space.vcs.SpaceRepoInfo +import com.intellij.space.vcs.review.SpaceReviewDataKeys +import git4idea.GitLocalBranch +import git4idea.GitStandardRemoteBranch +import git4idea.branch.GitBranchUiHandlerImpl +import git4idea.branch.GitBranchUtil +import git4idea.branch.GitBranchWorker +import git4idea.branch.GitNewBranchOptions +import git4idea.commands.Git +import git4idea.fetch.GitFetchSupport +import git4idea.repo.GitRepository +import org.jetbrains.annotations.Nullable + +class SpaceReviewCheckoutBranchAction : DumbAwareAction(SpaceBundle.messagePointer("review.actions.checkout.branch")) { + override fun update(e: AnActionEvent) { + val detailsVm = getMergeRequestVm(e) + + if (detailsVm == null) { + e.presentation.isEnabledAndVisible = false + return + } + + val repoInfo = detailsVm.repoInfo.value + if (repoInfo == null) { + e.presentation.isEnabledAndVisible = false + return + } + val localBranch = detailsVm.mergeRequestBranchInfo.value.localBranch + if (localBranch != null) { + if (repoInfo.repository.currentBranchName == localBranch.name) { + e.presentation.isEnabledAndVisible = false + return + } + } + e.presentation.isEnabledAndVisible = true + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val detailsVm = getMergeRequestVm(e) ?: return + val repoInfo = detailsVm.repoInfo.value ?: return + + val gitRepository = repoInfo.repository + val gitRepositories = listOf(gitRepository) + + val sourceBranchInfo = detailsVm.sourceBranchInfo.value ?: return + + val localBranch = detailsVm.mergeRequestBranchInfo.value.localBranch + + if (localBranch != null) { + checkoutLocalBranch(project, detailsVm, localBranch, gitRepositories) + } + else { + val remoteBranch = GitStandardRemoteBranch(repoInfo.remote, sourceBranchInfo.displayName) + + val options = GitBranchUtil.getNewBranchNameFromUser( + project, + gitRepositories, + SpaceBundle.message("review.checkout.action.checkout.dialog.title", remoteBranch.name), + sourceBranchInfo.displayName, + false) ?: return + + if (options.checkout) { + checkoutRemoteBranch(project, detailsVm, gitRepository, repoInfo, sourceBranchInfo, options, gitRepositories) + } + else { + object : Task.Backgroundable(project, + SpaceBundle.message("review.checkout.action.progress.title.creating.branch", detailsVm.reviewKey), + true) { + override fun run(indicator: ProgressIndicator) { + val git = Git.getInstance() + + GitFetchSupport.fetchSupport(project) + .fetch(gitRepository, repoInfo.remote) + .throwExceptionIfFailed() + + GitBranchWorker(project, git, GitBranchUiHandlerImpl(project, git, indicator)) + .createBranch(options.name, mapOf(gitRepository to sourceBranchInfo.ref)) + + gitRepository.update() + } + + }.queue() + } + } + } + + private fun checkoutRemoteBranch(project: @Nullable Project, + detailsVm: MergeRequestDetailsVm, + gitRepository: GitRepository, + repoInfo: SpaceRepoInfo, + sourceBranchInfo: MergeRequestBranch, + options: @Nullable GitNewBranchOptions, + gitRepositories: List) { + object : Task.Backgroundable(project, + SpaceBundle.message("review.checkout.action.progress.title.checking.out.branch", detailsVm.reviewKey), + true) { + override fun run(indicator: ProgressIndicator) { + val git = Git.getInstance() + + GitFetchSupport.fetchSupport(project) + .fetch(gitRepository, repoInfo.remote) + .throwExceptionIfFailed() + + val remoteBranch = GitStandardRemoteBranch(repoInfo.remote, sourceBranchInfo.displayName) + + GitBranchWorker(project, git, GitBranchUiHandlerImpl(project, git, indicator)) + .checkoutNewBranchStartingFrom(options.name, remoteBranch.name, gitRepositories) + + gitRepository.update() + } + }.queue() + } + + private fun checkoutLocalBranch(project: @Nullable Project, + detailsVm: MergeRequestDetailsVm, + localBranch: GitLocalBranch, + gitRepositories: List) { + object : Task.Backgroundable(project, + SpaceBundle.message("review.checkout.action.progress.title.checking.out.branch", detailsVm.reviewKey), + true) { + override fun run(indicator: ProgressIndicator) { + val git = Git.getInstance() + GitBranchWorker(project, git, GitBranchUiHandlerImpl(project, git, indicator)) + .checkout(localBranch.name, false, gitRepositories) + } + }.queue() + } +} + +class SpaceReviewUpdateBranchAction : DumbAwareAction(SpaceBundle.messagePointer("review.actions.update.branch")) { + override fun update(e: AnActionEvent) { + val detailsVm = getMergeRequestVm(e) + + if (detailsVm == null) { + e.presentation.isEnabledAndVisible = false + return + } + + val repoInfo = detailsVm.repoInfo.value + if (repoInfo == null) { + e.presentation.isEnabledAndVisible = false + return + } + val mergeRequestBranchInfo = detailsVm.mergeRequestBranchInfo.value + + if (mergeRequestBranchInfo.localBranch != null && mergeRequestBranchInfo.isCurrentBranch) { + e.presentation.isEnabledAndVisible = true + return + } + + e.presentation.isEnabledAndVisible = false + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val detailsVm = getMergeRequestVm(e) ?: return + + val gitRepository = detailsVm.repoInfo.value?.repository ?: return + val localBranch = detailsVm.mergeRequestBranchInfo.value.localBranch ?: return + + GitBranchUtil.updateBranches(project, listOf(gitRepository), listOf(localBranch.name)) + } +} + +private fun getMergeRequestVm(e: AnActionEvent): MergeRequestDetailsVm? { + val detailsVm = e.getData(SpaceReviewDataKeys.REVIEW_DETAILS_VM) + + return if (detailsVm is MergeRequestDetailsVm) detailsVm else null +} \ No newline at end of file diff --git a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetails.kt b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetails.kt index 531a15b29b74..b9ad3acf570b 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetails.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetails.kt @@ -44,9 +44,10 @@ internal class SpaceReviewDetails(parentDisposable: Disposable, uiDisposable?.let { Disposer.dispose(it) } if (reviewListItem == null) return@forEach val detailsLifetime = sequentialLifetimes.next() - val detailsVm = createReviewDetailsVm(detailsLifetime, project, workspace, spaceProjectInfo, repoInfo, reviewListItem) - uiDisposable = Disposer.newDisposable() + + val detailsVm = createReviewDetailsVm(uiDisposable!!, detailsLifetime, project, workspace, spaceProjectInfo, repoInfo, reviewListItem) + Disposer.register(parentDisposable, uiDisposable as Disposable) val detailsTabInfo = TabInfo(SpaceReviewInfoTabPanel(parentDisposable, detailsVm)).apply { diff --git a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetailsVm.kt b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetailsVm.kt index 55f8b880d582..4eb2f1b12d6d 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetailsVm.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewDetailsVm.kt @@ -10,7 +10,10 @@ import circlet.platform.api.Ref import circlet.platform.api.TID import circlet.platform.client.* import circlet.workspaces.Workspace +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.space.utils.SpaceUrls import com.intellij.space.vcs.SpaceProjectInfo import com.intellij.space.vcs.SpaceRepoInfo @@ -19,6 +22,9 @@ import com.intellij.space.vcs.review.details.diff.SpaceDiffVmImpl import com.intellij.space.vcs.review.details.diff.SpaceReviewDiffLoader import com.intellij.space.vcs.review.details.process.SpaceReviewStateUpdater import com.intellij.space.vcs.review.details.process.SpaceReviewStateUpdaterImpl +import com.intellij.vcs.log.data.DataPackChangeListener +import com.intellij.vcs.log.impl.VcsProjectLog +import git4idea.GitLocalBranch import libraries.coroutines.extra.Lifetime import libraries.coroutines.extra.Lifetimed import runtime.reactive.* @@ -29,7 +35,7 @@ internal sealed class SpaceReviewDetailsVm( final override val lifetime: Lifetime, val ideaProject: Project, val spaceProjectInfo: SpaceProjectInfo, - spaceReposInfo: Set, + val spaceReposInfo: Set, val reviewRef: Ref, val workspace: Workspace ) : Lifetimed { @@ -56,7 +62,7 @@ internal sealed class SpaceReviewDetailsVm( val reviewStateUpdater: SpaceReviewStateUpdater = SpaceReviewStateUpdaterImpl(workspace, review.value) - private val infoByRepos = spaceReposInfo.associateBy(SpaceRepoInfo::name) + val infoByRepos = spaceReposInfo.associateBy(SpaceRepoInfo::name) private val participantsProperty: Property>> = load { client.arena.resolveRefsOrFetch { @@ -129,6 +135,7 @@ private fun selectedOrAll(tab: SelectedTab, selected: T, all: T): T = when ( } internal class MergeRequestDetailsVm( + parentDisposable: Disposable, lifetime: Lifetime, ideaProject: Project, spaceProjectInfo: SpaceProjectInfo, @@ -142,6 +149,67 @@ internal class MergeRequestDetailsVm( val repository: Property = cellProperty { branchPair.live.repository } val targetBranchInfo: Property = cellProperty { branchPair.live.targetBranchInfo } val sourceBranchInfo: Property = cellProperty { branchPair.live.sourceBranchInfo } + + val repoInfo: Property = map(repository) { repoName -> + getRepoInfo(repoName) + } + + val mergeRequestBranchInfo: MutableProperty = mutableProperty(MergeRequestBranchInfo(null, false)) + + private fun getRepoInfo(repoName: String): SpaceRepoInfo? { + return spaceReposInfo.firstOrNull { it.name == repoName } + } + + init { + review.forEach(lifetime) { + updateLocalBranch() + } + + val dataPackListener = DataPackChangeListener { + updateLocalBranch() + } + + VcsProjectLog.runWhenLogIsReady(ideaProject) { + val log = VcsProjectLog.getInstance(ideaProject) + val dataManager = log.dataManager ?: return@runWhenLogIsReady + ApplicationManager.getApplication().invokeLater( + { + dataManager.addDataPackChangeListener(dataPackListener) + Disposer.register(parentDisposable, Disposable { dataManager.removeDataPackChangeListener(dataPackListener) }) + }, + { + Disposer.isDisposed(parentDisposable) + } + ) + } + } + + private fun updateLocalBranch() { + val localBranch = kotlin.run { + val repoInfo = repoInfo.value ?: return@run null + val branch = sourceBranchInfo.value ?: return@run null + findLocalBranch(repoInfo, branch) + } + + val isCurrentBranch = kotlin.run { + val gitLocalBranch = localBranch ?: return@run false + val spaceRepoInfo = repoInfo.value ?: return@run false + return@run spaceRepoInfo.repository.currentBranch == gitLocalBranch + } + + mergeRequestBranchInfo.value = MergeRequestBranchInfo(localBranch, isCurrentBranch) + } + + private fun findLocalBranch(repoInfo: SpaceRepoInfo, branch: MergeRequestBranch): GitLocalBranch? { + val gitRepository = repoInfo.repository + val allBranches = gitRepository.branches + + val remoteBranch = allBranches.remoteBranches + .find { remoteBranch -> remoteBranch.nameForRemoteOperations == branch.displayName } + + return allBranches.localBranches + .firstOrNull { it.findTrackedBranch(gitRepository) == remoteBranch } + } } internal class CommitSetReviewDetailsVm( @@ -153,7 +221,8 @@ internal class CommitSetReviewDetailsVm( workspace: Workspace ) : SpaceReviewDetailsVm(lifetime, ideaProject, spaceProjectInfo, spaceReposInfo, refMrRecord, workspace) -internal fun createReviewDetailsVm(lifetime: Lifetime, +internal fun createReviewDetailsVm(disposable: Disposable, + lifetime: Lifetime, project: Project, workspace: Workspace, spaceProjectInfo: SpaceProjectInfo, @@ -162,6 +231,7 @@ internal fun createReviewDetailsVm(lifetime: Lifetime, val client = workspace.client return when (val codeReviewRecord = codeReviewListItem.review.resolve()) { is MergeRequestRecord -> MergeRequestDetailsVm( + disposable, lifetime, project, spaceProjectInfo, @@ -192,4 +262,17 @@ private fun SpaceReviewDetailsVm<*>.pendingCounterAsync(client: KCircletClient): reviewRef.extensionRef(CodeReviewPendingMessageCounter::class) } } +} + +internal class MergeRequestBranchInfo( + val localBranch: GitLocalBranch?, + val isCurrentBranch: Boolean +) + +internal fun SpaceReviewDetailsVm.isMergeRequest(): Boolean { + return this is MergeRequestDetailsVm +} + +internal fun SpaceReviewDetailsVm.isCommitSetReview(): Boolean { + return this is CommitSetReviewDetailsVm } \ No newline at end of file diff --git a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewInfoPanelFactory.kt b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewInfoPanelFactory.kt index 13b0846fe61f..bd206265f7d0 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewInfoPanelFactory.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/vcs/review/details/SpaceReviewInfoPanelFactory.kt @@ -3,6 +3,7 @@ package com.intellij.space.vcs.review.details import circlet.client.api.englishFullName import com.intellij.ide.plugins.newui.VerticalLayout +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsSafe @@ -19,6 +20,7 @@ import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBOptionButton +import com.intellij.ui.components.labels.ActionLink import com.intellij.ui.components.labels.LinkLabel import com.intellij.ui.components.panels.NonOpaquePanel import com.intellij.util.FontUtil @@ -29,6 +31,7 @@ import com.intellij.util.ui.components.BorderLayoutPanel import net.miginfocom.layout.CC import net.miginfocom.layout.LC import net.miginfocom.swing.MigLayout +import runtime.reactive.view import java.awt.FlowLayout import java.beans.PropertyChangeListener import javax.swing.* @@ -121,7 +124,7 @@ internal object SpaceReviewInfoPanelFactory { add(projectDetails) if (detailsVm is MergeRequestDetailsVm) { - add(createDirectionPanel(detailsVm)) + add(createBranchesPanel(this, detailsVm)) } add(titleComponent) @@ -143,6 +146,32 @@ internal object SpaceReviewInfoPanelFactory { } } + private fun createBranchesPanel(parent: JPanel, detailsVm: MergeRequestDetailsVm): BorderLayoutPanel { + val actionManager = ActionManager.getInstance() + + val checkoutAction = actionManager.getAction("com.intellij.space.vcs.review.details.SpaceReviewCheckoutBranchAction") + val checkoutActionLink = ActionLink(SpaceBundle.message("review.actions.link.label.checkout"), checkoutAction) + + val updateAction = actionManager.getAction("com.intellij.space.vcs.review.details.SpaceReviewUpdateBranchAction") + val updateActionLink = ActionLink(SpaceBundle.message("review.actions.link.label.update"), updateAction) + + val branchActionPanel = NonOpaquePanel().apply { + border = JBUI.Borders.emptyLeft(4) + } + + val branchesPanel = BorderLayoutPanel() + .addToCenter(createDirectionPanel(detailsVm)) + .addToRight(branchActionPanel) + .andTransparent() + + detailsVm.mergeRequestBranchInfo.view(detailsVm.lifetime) { _, mergeRequestBranchInfo -> + val action = if (mergeRequestBranchInfo.isCurrentBranch) updateActionLink else checkoutActionLink + branchActionPanel.setContent(action) + parent.repaint() + } + return branchesPanel + } + private fun createActionButton(detailsVm: SpaceReviewDetailsVm<*>, controlVM: ParticipantStateControlVM): JComponent? { val actionsBuilder = SpaceReviewActionsBuilder(detailsVm.reviewStateUpdater) return when (controlVM) { diff --git a/plugins/space/src/main/resources/META-INF/plugin.xml b/plugins/space/src/main/resources/META-INF/plugin.xml index bccb709df51f..dbdb566e3d43 100644 --- a/plugins/space/src/main/resources/META-INF/plugin.xml +++ b/plugins/space/src/main/resources/META-INF/plugin.xml @@ -131,9 +131,14 @@ - + + + + @@ -143,6 +148,7 @@ + messages.SpaceBundle diff --git a/plugins/space/src/main/resources/messages/SpaceBundle.properties b/plugins/space/src/main/resources/messages/SpaceBundle.properties index 0fcda662a6e1..08b3c7b989f6 100644 --- a/plugins/space/src/main/resources/messages/SpaceBundle.properties +++ b/plugins/space/src/main/resources/messages/SpaceBundle.properties @@ -205,6 +205,14 @@ review.participant.tooltip.wait.response=Waits for a response review.actions.wait.for.response=Wait for Response review.actions.resume.review=Resume review review.actions.accept.changes=Accept Changes - +review.actions.link.label.checkout=Checkout +review.actions.link.label.update=Update +review.checkout.action.checkout.dialog.title=Checkout {0} +review.actions.update.branch=Update Branch +review.actions.checkout.branch=Checkout Branch... +review.checkout.action.progress.title.checking.out.branch=Checking out branch from {0} +review.checkout.action.progress.title.creating.branch=Creating branch from {0} space.task.run.configuration.name=Space Task + +