[space] Add ability to checkout MR branch

GitOrigin-RevId: 342cd559de34cd7796b155984d0248d93e43150a
This commit is contained in:
Aleksandr Krasilnikov
2021-01-27 14:14:22 +00:00
committed by intellij-monorepo-bot
parent d19a4b0d71
commit 558e66f0b0
6 changed files with 315 additions and 8 deletions
@@ -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<GitRepository>) {
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<GitRepository>) {
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
}
@@ -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 {
@@ -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<R : CodeReviewRecord>(
final override val lifetime: Lifetime,
val ideaProject: Project,
val spaceProjectInfo: SpaceProjectInfo,
spaceReposInfo: Set<SpaceRepoInfo>,
val spaceReposInfo: Set<SpaceRepoInfo>,
val reviewRef: Ref<R>,
val workspace: Workspace
) : Lifetimed {
@@ -56,7 +62,7 @@ internal sealed class SpaceReviewDetailsVm<R : CodeReviewRecord>(
val reviewStateUpdater: SpaceReviewStateUpdater = SpaceReviewStateUpdaterImpl(workspace, review.value)
private val infoByRepos = spaceReposInfo.associateBy(SpaceRepoInfo::name)
val infoByRepos = spaceReposInfo.associateBy(SpaceRepoInfo::name)
private val participantsProperty: Property<LoadingValue<Ref<CodeReviewParticipants>>> = load {
client.arena.resolveRefsOrFetch {
@@ -129,6 +135,7 @@ private fun <T> 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<String> = cellProperty { branchPair.live.repository }
val targetBranchInfo: Property<MergeRequestBranch?> = cellProperty { branchPair.live.targetBranchInfo }
val sourceBranchInfo: Property<MergeRequestBranch?> = cellProperty { branchPair.live.sourceBranchInfo }
val repoInfo: Property<SpaceRepoInfo?> = map(repository) { repoName ->
getRepoInfo(repoName)
}
val mergeRequestBranchInfo: MutableProperty<MergeRequestBranchInfo> = 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<CommitSetReviewRecord>(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 <R : CodeReviewRecord> SpaceReviewDetailsVm<R>.isMergeRequest(): Boolean {
return this is MergeRequestDetailsVm
}
internal fun <R : CodeReviewRecord> SpaceReviewDetailsVm<R>.isCommitSetReview(): Boolean {
return this is CommitSetReviewDetailsVm
}
@@ -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) {
@@ -131,9 +131,14 @@
<separator/>
<action id="com.intellij.space.vcs.review.list.SpaceReviewAuthorActionGroup"
class="com.intellij.space.vcs.review.list.SpaceReviewAuthorActionGroup"/>
</group>
<action id="com.intellij.space.vcs.review.details.SpaceReviewCheckoutBranchAction"
class="com.intellij.space.vcs.review.details.SpaceReviewCheckoutBranchAction"/>
<action id="com.intellij.space.vcs.review.details.SpaceReviewUpdateBranchAction"
class="com.intellij.space.vcs.review.details.SpaceReviewUpdateBranchAction"/>
<group id="space.review.changes.toolbar">
<reference id="Diff.ShowDiff"/>
<reference id="EditSource"/>
@@ -143,6 +148,7 @@
<reference id="Diff.ShowDiff"/>
<reference id="EditSource"/>
</group>
</actions>
<resource-bundle>messages.SpaceBundle</resource-bundle>
@@ -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