diff --git a/platform/smart-update/src/com/intellij/smartUpdate/VcsUpdateStep.kt b/platform/smart-update/src/com/intellij/smartUpdate/VcsUpdateStep.kt index e64f5d35a9e8..b7a556d1d1b0 100644 --- a/platform/smart-update/src/com/intellij/smartUpdate/VcsUpdateStep.kt +++ b/platform/smart-update/src/com/intellij/smartUpdate/VcsUpdateStep.kt @@ -1,14 +1,12 @@ package com.intellij.smartUpdate -import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.impl.SimpleDataContext import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.update.ActionInfo -import com.intellij.openapi.vcs.update.CommonUpdateProjectAction import com.intellij.openapi.vcs.update.ScopeInfo +import com.intellij.openapi.vcs.update.VcsUpdateProcess import com.intellij.ui.dsl.builder.panel import com.intellij.ui.layout.ComponentPredicate import javax.swing.JComponent @@ -16,30 +14,24 @@ import javax.swing.JComponent private const val VCS_UPDATE = "vcs.update" internal class VcsUpdateStep : SmartUpdateStep { + private val actionInfo = ActionInfo.UPDATE + private val scopeInfo = ScopeInfo.PROJECT + private lateinit var showOptionsListener: (Boolean) -> Unit override val id: String = VCS_UPDATE override val stepName = SmartUpdateBundle.message("checkbox.update.project") override fun performUpdateStep(project: Project, e: AnActionEvent?, onSuccess: () -> Unit) { val start = System.currentTimeMillis() - val action = object : CommonUpdateProjectAction() { - override fun isShowOptions(project: Project?) = false - - override fun onSuccess() { - SmartUpdateUsagesCollector.logUpdate(System.currentTimeMillis() - start) - onSuccess.invoke() - } + val dataContext = SimpleDataContext.getProjectContext(project) + VcsUpdateProcess.launchUpdate(project, actionInfo, scopeInfo, dataContext, false, SmartUpdateBundle.message("action.update.project.text")) { + SmartUpdateUsagesCollector.logUpdate(System.currentTimeMillis() - start) + onSuccess.invoke() } - action.templatePresentation.text = SmartUpdateBundle.message("action.update.project.text") - val dataContext = SimpleDataContext.builder() - .add(CommonDataKeys.PROJECT, project) - .build() - val actionEvent = AnActionEvent.createFromAnAction(action, null, ActionPlaces.UNKNOWN, dataContext) - action.actionPerformed(actionEvent) } override fun getDetailsComponent(project: Project): JComponent? { - if (!ActionInfo.UPDATE.showOptions(project)) return super.getDetailsComponent(project) + if (!actionInfo.showOptions(project)) return super.getDetailsComponent(project) return panel { row { label(SmartUpdateBundle.message("warning.default.update.options.will.be.applied")) } row { link(SmartUpdateBundle.message("label.change.options")) { showOptionsDialog(project) } } @@ -50,9 +42,9 @@ internal class VcsUpdateStep : SmartUpdateStep { val showOptions = ActionInfo.UPDATE.showOptions(project) showOptionsListener.invoke(showOptions) if (showOptions) { - val map = CommonUpdateProjectAction().getConfigurableToEnvMap(project) - val scopeName = ScopeInfo.PROJECT.getScopeName(DataContext.EMPTY_CONTEXT, ActionInfo.UPDATE) - ActionInfo.UPDATE.createOptionsDialog(project, map, scopeName).show() + val roots = VcsUpdateProcess.getRoots(project, actionInfo, scopeInfo, SimpleDataContext.getProjectContext(project)) + val spec = VcsUpdateProcess.createUpdateSpec(project, roots, actionInfo) + VcsUpdateProcess.showOptionsDialog(project, actionInfo, scopeInfo, spec, DataContext.EMPTY_CONTEXT) } } @@ -62,7 +54,7 @@ internal class VcsUpdateStep : SmartUpdateStep { showOptionsListener = listener } - override fun invoke() = ActionInfo.UPDATE.showOptions(project) + override fun invoke() = actionInfo.showOptions(project) } } } diff --git a/platform/vcs-impl/api-dump-experimental.txt b/platform/vcs-impl/api-dump-experimental.txt index a43b8694d97b..56dc59858cb2 100644 --- a/platform/vcs-impl/api-dump-experimental.txt +++ b/platform/vcs-impl/api-dump-experimental.txt @@ -17,6 +17,11 @@ c:com.intellij.openapi.vcs.changes.ui.TreeModelBuilder - com.intellij.openapi.vcs.changes.ui.ChangesViewModelBuilder - *:build(Z):javax.swing.tree.DefaultTreeModel +*f:com.intellij.openapi.vcs.update.VcsUpdateProcess +- sf:INSTANCE:com.intellij.openapi.vcs.update.VcsUpdateProcess +- sf:checkUpdateHasCustomNotification(java.util.Collection):Z +- f:launchUpdate(com.intellij.openapi.project.Project,com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,com.intellij.openapi.actionSystem.DataContext,java.lang.String,Z):V +- bs:launchUpdate$default(com.intellij.openapi.vcs.update.VcsUpdateProcess,com.intellij.openapi.project.Project,com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,com.intellij.openapi.actionSystem.DataContext,java.lang.String,Z,I,java.lang.Object):V *:com.intellij.vcs.CacheableAnnotationProvider - a:getFromCache(com.intellij.openapi.vfs.VirtualFile):com.intellij.openapi.vcs.annotate.FileAnnotation - a:populateCache(com.intellij.openapi.vfs.VirtualFile):V diff --git a/platform/vcs-impl/api-dump.txt b/platform/vcs-impl/api-dump.txt index 7e5f35e0741d..acae35fa73a9 100644 --- a/platform/vcs-impl/api-dump.txt +++ b/platform/vcs-impl/api-dump.txt @@ -4068,20 +4068,6 @@ c:com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier - run():V - s:showOverChangesView(com.intellij.openapi.project.Project,java.lang.String,com.intellij.openapi.ui.MessageType,com.intellij.openapi.util.NamedRunnable[]):V - s:showOverVersionControlView(com.intellij.openapi.project.Project,java.lang.String,com.intellij.openapi.ui.MessageType):V -a:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction -- com.intellij.openapi.project.DumbAwareAction -- sf:Companion:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction$Companion -- p:(com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,Z):V -- actionPerformed(com.intellij.openapi.actionSystem.AnActionEvent):V -- pa:filterRootsBeforeAction():Z -- getActionUpdateThread():com.intellij.openapi.actionSystem.ActionUpdateThread -- f:getConfigurableToEnvMap(com.intellij.openapi.project.Project):java.util.LinkedHashMap -- p:isShowOptions(com.intellij.openapi.project.Project):Z -- p:onSuccess():V -- sf:showsCustomNotification(java.util.Collection):Z -- update(com.intellij.openapi.actionSystem.AnActionEvent):V -f:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction$Companion -- f:showsCustomNotification(java.util.Collection):Z com.intellij.openapi.vcs.update.ActionInfo - sf:INTEGRATE:com.intellij.openapi.vcs.update.ActionInfo - sf:STATUS:com.intellij.openapi.vcs.update.ActionInfo @@ -4094,10 +4080,6 @@ com.intellij.openapi.vcs.update.ActionInfo - a:getEnvironment(com.intellij.openapi.vcs.AbstractVcs):com.intellij.openapi.vcs.update.UpdateEnvironment - a:getGroupName(com.intellij.openapi.vcs.update.FileGroup):java.lang.String - a:showOptions(com.intellij.openapi.project.Project):Z -c:com.intellij.openapi.vcs.update.CommonUpdateProjectAction -- com.intellij.openapi.vcs.update.AbstractCommonUpdateAction -- ():V -- p:filterRootsBeforeAction():Z com.intellij.openapi.vcs.update.FilePathChange - a:getAfterPath():com.intellij.openapi.vcs.FilePath - a:getBeforePath():com.intellij.openapi.vcs.FilePath diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/update/AbstractCommonUpdateAction.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/AbstractCommonUpdateAction.kt index e199cf863897..dc76a5c082e0 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/update/AbstractCommonUpdateAction.kt +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/AbstractCommonUpdateAction.kt @@ -4,7 +4,6 @@ package com.intellij.openapi.vcs.update import com.intellij.configurationStore.StoreReloadManager -import com.intellij.configurationStore.forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod import com.intellij.history.Label import com.intellij.history.LocalHistory import com.intellij.history.LocalHistoryAction @@ -13,14 +12,10 @@ import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.impl.SimpleDataContext -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.debug -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.options.Configurable -import com.intellij.openapi.progress.* +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsContexts @@ -28,7 +23,6 @@ import com.intellij.openapi.util.Ref import com.intellij.openapi.util.text.HtmlBuilder import com.intellij.openapi.util.text.HtmlChunk import com.intellij.openapi.vcs.* -import com.intellij.openapi.vcs.actions.DescindingFilesFilter import com.intellij.openapi.vcs.changes.RemoteRevisionsCache import com.intellij.openapi.vcs.changes.VcsAnnotationRefresher import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager @@ -38,178 +32,40 @@ import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.util.containers.MultiMap import com.intellij.util.ui.OptionsDialog import com.intellij.vcs.VcsActivity import com.intellij.vcs.ViewUpdateInfoNotification -import com.intellij.vcsUtil.VcsUtil import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls private val LOG = Logger.getInstance(AbstractCommonUpdateAction::class.java) +@ApiStatus.Internal abstract class AbstractCommonUpdateAction protected constructor( private val actionInfo: ActionInfo, private val scopeInfo: ScopeInfo, - private val alwaysVisible: Boolean + private val alwaysVisible: Boolean, ) : DumbAwareAction() { companion object { + @Deprecated("Use VcsUpdateProcess.checkUpdateHasCustomNotification", + ReplaceWith("VcsUpdateProcess.checkUpdateHasCustomNotification(vcss)", "com.intellij.openapi.vcs.update.VcsUpdateProcess")) @JvmStatic - fun showsCustomNotification(vcss: Collection): Boolean { - return vcss.all { vcs -> - val environment = vcs.updateEnvironment - environment != null && environment.hasCustomNotification() - } - } + fun showsCustomNotification(vcss: Collection): Boolean = VcsUpdateProcess.checkUpdateHasCustomNotification(vcss) } override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT - @Suppress("IncorrectCancellationExceptionHandling") override fun actionPerformed(e: AnActionEvent) { - val project = e.project - val showUpdateOptions = isShowOptions(project) - - LOG.debug { "project: $project, show update options: $showUpdateOptions" } - - if (project == null) { - return - } - - try { - val roots = getRoots(project, e.dataContext) - if (roots.isEmpty()) { - LOG.debug("No roots found.") - return - } - - val vcsToVirtualFiles = createVcsToFilesMap(roots, project) - for (vcs in vcsToVirtualFiles.keys) { - val updateEnvironment = actionInfo.getEnvironment(vcs) - if (updateEnvironment != null && !updateEnvironment.validateOptions(vcsToVirtualFiles.get(vcs))) { - // messages already shown - LOG.debug { "Options not valid for files: $vcsToVirtualFiles" } - return - } - } - - if (showUpdateOptions || OptionsDialog.shiftIsPressed(e.modifiers)) { - val scopeName = scopeInfo.getScopeName(e.dataContext, actionInfo) - showOptionsDialog(vcsToVirtualFiles, project, scopeName) - } - - if (ApplicationManager.getApplication().isDispatchThread()) { - // Not only documents, but also project settings should be saved, - // to ensure that if as a result of Update some project settings will be changed, - // all local changes are saved in prior and do not overwrite remote changes. - // Also, there is a chance that save during update can break it - - // we do disable auto saving during update, but still, there is a chance that save will occur. - FileDocumentManager.getInstance().saveAllDocuments() - forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod(project, false) - } - - val task = object : Updater(project, roots, vcsToVirtualFiles, actionInfo, getTemplatePresentation().text) { - override fun onSuccess() { - super.onSuccess() - this@AbstractCommonUpdateAction.onSuccess() - } - } - - if (ApplicationManager.getApplication().isUnitTestMode()) { - task.run(EmptyProgressIndicator()) - } - else { - ProgressManager.getInstance().run(task) - } - } - catch (_: ProcessCanceledException) { - } - } - - protected open fun isShowOptions(project: Project?): Boolean = actionInfo.showOptions(project) - - protected open fun onSuccess() { - } - - private fun showOptionsDialog( - updateEnvToVirtualFiles: Map>, project: Project?, - scopeName: String? - ) { - val envToConfMap = createConfigurableToEnvMap(updateEnvToVirtualFiles) - LOG.debug { "configurables map: $envToConfMap" } - if (!envToConfMap.isEmpty()) { - val dialogOrStatus = actionInfo.createOptionsDialog(project, envToConfMap, scopeName) - if (!dialogOrStatus.showAndGet()) { - throw ProcessCanceledException() - } - } - } - - private fun getRoots(project: Project, context: DataContext): Array { - val filePaths = scopeInfo.getRoots(context, actionInfo) - return DescindingFilesFilter.filterDescindingFiles(filterRoots(project, filePaths), project) - } - - private fun createConfigurableToEnvMap(updateEnvToVirtualFiles: Map>): LinkedHashMap { - val envToConfMap = LinkedHashMap() - for (vcs in updateEnvToVirtualFiles.keys) { - val configurable = actionInfo.getEnvironment(vcs).createConfigurable(updateEnvToVirtualFiles.get(vcs)) - if (configurable != null) { - envToConfMap.put(configurable, vcs) - } - } - return envToConfMap - } - - fun getConfigurableToEnvMap(project: Project): LinkedHashMap { - val roots = getRoots(project, SimpleDataContext.getProjectContext(project)) - val vcsToFilesMap = createVcsToFilesMap(roots, project) - return createConfigurableToEnvMap(vcsToFilesMap) - } - - private fun createVcsToFilesMap(roots: Array, project: Project): Map> { - val resultPrep = MultiMap.createSet() - for (file in roots) { - val vcs = VcsUtil.getVcsFor(project, file) ?: continue - val updateEnvironment = actionInfo.getEnvironment(vcs) - if (updateEnvironment != null) { - resultPrep.putValue(vcs, file) - } - } - - val result = HashMap>() - for (entry in resultPrep.entrySet()) { - val vcs = entry.key - @Suppress("DEPRECATION") - result.put(vcs, vcs.filterUniqueRoots(ArrayList(entry.value)) { it.getVirtualFile() }) - } - return result - } - - private fun filterRoots(project: Project, roots: MutableList): Array { - val result = ArrayList() - for (file in roots) { - val vcs = VcsUtil.getVcsFor(project, file) ?: continue - if (!scopeInfo.filterExistsInVcs() || AbstractVcs.fileInVcsByFileStatus(project, file)) { - val updateEnvironment = actionInfo.getEnvironment(vcs) - if (updateEnvironment != null) { - result.add(file) - } - } - else { - val virtualFile = file.getVirtualFile() - if (virtualFile != null && virtualFile.isDirectory()) { - val vcsRoots = ProjectLevelVcsManager.getInstance(project).getAllVersionedRoots() - for (vcsRoot in vcsRoots) { - if (VfsUtilCore.isAncestor(virtualFile, vcsRoot, false)) { - result.add(file) - } - } - } - } - } - return result.toTypedArray() + val project = e.project ?: return + VcsUpdateProcess.launchUpdate( + project, + actionInfo, + scopeInfo, + e.dataContext, + actionName = getTemplatePresentation().text, + forceShowOptions = OptionsDialog.shiftIsPressed(e.modifiers), + ) } protected abstract fun filterRootsBeforeAction(): Boolean @@ -243,8 +99,7 @@ abstract class AbstractCommonUpdateAction protected constructor( } if (filterRootsBeforeAction()) { - val filePaths = scopeInfo.getRoots(e.dataContext, actionInfo) - val roots = filterRoots(project, filePaths) + val roots = VcsUpdateProcess.getRoots(project, actionInfo, scopeInfo, e.dataContext, false) if (roots.isEmpty()) { presentation.setVisible(alwaysVisible) presentation.setEnabled(false) @@ -257,13 +112,12 @@ abstract class AbstractCommonUpdateAction protected constructor( presentation.setEnabled(!vcsManager.isBackgroundVcsOperationRunning() && (singleVcs == null || !singleVcs.isUpdateActionDisabled)) } - @ApiStatus.Internal - open class Updater( - project: Project, + internal open class Updater( + private val project: Project, private val roots: Array, - private val vcsToVirtualFiles: Map>, + private val spec: List, private val actionInfo: ActionInfo, - private val actionName: @Nls @NlsContexts.ProgressTitle String + private val actionName: @Nls @NlsContexts.ProgressTitle String, ) : Task.Backgroundable(project, actionName, true) { private val projectLevelVcsManager = ProjectLevelVcsManagerEx.getInstanceEx(project) protected var updatedFiles: UpdatedFiles = UpdatedFiles.create() @@ -292,31 +146,25 @@ abstract class AbstractCommonUpdateAction protected constructor( } private fun runImpl() { - val project = myProject - if (project != null) { - StoreReloadManager.getInstance(project).blockReloadingProjectOnExternalChanges() - } + StoreReloadManager.getInstance(project).blockReloadingProjectOnExternalChanges() projectLevelVcsManager.startBackgroundVcsOperation() val progressIndicator = ProgressManager.getInstance().getProgressIndicator() - before = LocalHistory.getInstance().putSystemLabel(project!!, VcsBundle.message("update.label.before.update")) + before = LocalHistory.getInstance().putSystemLabel(project, VcsBundle.message("update.label.before.update")) localHistoryAction = LocalHistory.getInstance().startAction(VcsBundle.message("activity.name.update"), VcsActivity.Update) progressIndicator?.setIndeterminate(false) val activity = VcsStatisticsCollector.UPDATE_ACTIVITY.started(project) try { - val toBeProcessed = vcsToVirtualFiles.size + val toBeProcessed = spec.size var processed = 0 - for (vcs in vcsToVirtualFiles.keys) { - val updateEnvironment = actionInfo.getEnvironment(vcs) + for ((vcs, updateEnvironment, files) in spec) { updateEnvironment.fillGroups(updatedFiles) - val files = vcsToVirtualFiles.get(vcs)!! val context = contextInfo.get(vcs) val refContext = Ref(context) - // actual update - val updateSession = performUpdate(progressIndicator, updateEnvironment, files, refContext) + val updateSession = updateEnvironment.updateDirectories(files.toTypedArray(), updatedFiles, progressIndicator, refContext) contextInfo.put(vcs, refContext.get()) processed++ @@ -343,15 +191,6 @@ abstract class AbstractCommonUpdateAction protected constructor( } } - protected open fun performUpdate( - progressIndicator: ProgressIndicator?, - updateEnvironment: UpdateEnvironment, - files: Collection, - refContext: Ref - ): UpdateSession { - return updateEnvironment.updateDirectories(files.toTypedArray(), updatedFiles, progressIndicator, refContext) - } - private fun gatherExceptions(vcs: AbstractVcs, exceptionList: MutableList) { val fixer = vcs.vcsExceptionsHotFixer if (fixer == null) { @@ -389,7 +228,7 @@ abstract class AbstractCommonUpdateAction protected constructor( private fun prepareNotification( tree: UpdateInfoTree, someSessionWasCancelled: Boolean, - updateSessions: List + updateSessions: List, ): Notification { val allFilesCount = updatedFiles.topLevelGroups.sumOf { getFilesCount(it) } @@ -507,7 +346,7 @@ abstract class AbstractCommonUpdateAction protected constructor( .setDisplayId(VcsNotificationIdsHolder.PROJECT_UPDATE_FINISHED)) } else if (!updatedFiles.isEmpty) { - if (updateSessions.size == 1 && showsCustomNotification(vcsToVirtualFiles.keys)) { + if (updateSessions.size == 1 && VcsUpdateProcess.checkUpdateHasCustomNotification(spec.map { it.vcs })) { // multi-vcs projects behave as before: only a compound notification & file tree is shown for them, for the sake of simplicity updateSessions.get(0).showNotification() } @@ -563,13 +402,13 @@ abstract class AbstractCommonUpdateAction protected constructor( val updateInfoTree = projectLevelVcsManager.showUpdateProjectInfo(updatedFiles, text, actionInfo, wasCanceled)!! updateInfoTree.setBefore(before) updateInfoTree.setAfter(after) - updateInfoTree.setCanGroupByChangeList(canGroupByChangelist(vcsToVirtualFiles.keys)) + updateInfoTree.setCanGroupByChangeList(canGroupByChangelist(spec.map { it.vcs })) return updateInfoTree } - private fun canGroupByChangelist(abstractVcses: Set): Boolean { + private fun canGroupByChangelist(vcses: Collection): Boolean { if (actionInfo.canGroupByChangelist()) { - for (vcs in abstractVcses) { + for (vcs in vcses) { if (vcs.getCachingCommittedChangesProvider() != null) { return true } @@ -598,7 +437,7 @@ private fun prepareScopeUpdatedText(tree: UpdateInfoTree): @Nls HtmlChunk { } } -private fun someSessionWasCanceled(updateSessions: List): Boolean = updateSessions.any { it.isCanceled() } +private fun someSessionWasCanceled(updateSessions: List): Boolean = updateSessions.any { it.isCanceled() } private fun getAllFilesAreUpToDateMessage(roots: Array): @NlsContexts.NotificationContent String { if (roots.size == 1 && !roots[0].isDirectory()) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/update/CommonUpdateProjectAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/CommonUpdateProjectAction.java index 26f2cd3d5374..7056cc996026 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/update/CommonUpdateProjectAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/CommonUpdateProjectAction.java @@ -15,6 +15,9 @@ */ package com.intellij.openapi.vcs.update; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public class CommonUpdateProjectAction extends AbstractCommonUpdateAction { public CommonUpdateProjectAction() { super(ActionInfo.UPDATE, ScopeInfo.PROJECT, true); diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/update/VcsUpdateProcess.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/VcsUpdateProcess.kt new file mode 100644 index 000000000000..f08043675223 --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/update/VcsUpdateProcess.kt @@ -0,0 +1,244 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.vcs.update + +import com.intellij.configurationStore.forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.debug +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.AbstractVcs +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.actions.DescindingFilesFilter +import com.intellij.openapi.vcs.update.AbstractCommonUpdateAction.Updater +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.containers.SmartHashSet +import com.intellij.vcsUtil.VcsUtil +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls + +@ApiStatus.Experimental +object VcsUpdateProcess { + @RequiresEdt + fun launchUpdate( + project: Project, + actionInfo: ActionInfo, + scopeInfo: ScopeInfo, + context: DataContext, + @Nls actionName: String, + forceShowOptions: Boolean = false, + ) { + val showUpdateOptions = actionInfo.showOptions(project) || forceShowOptions + launchUpdate(project, actionInfo, scopeInfo, context, showUpdateOptions, actionName) + } + + @ApiStatus.Internal + @RequiresEdt + fun launchUpdate( + project: Project, + actionInfo: ActionInfo, + scopeInfo: ScopeInfo, + context: DataContext, + showUpdateOptions: Boolean, + @Nls actionName: String, + @RequiresEdt onSuccess: () -> Unit = {}, + ) { + LOG.debug { "project: $project, show update options: $showUpdateOptions" } + + val roots = getRoots(project, actionInfo, scopeInfo, context) + if (roots.isEmpty()) { + LOG.debug { "No roots found." } + return + } + + val updateSpec = createUpdateSpec(project, roots, actionInfo) + if (!isUpdateSpecValid(updateSpec)) { + LOG.debug { "Options not valid for update spec: $updateSpec" } + return + } + + if (showUpdateOptions) { + val dialogOk = showOptionsDialog(project, actionInfo, scopeInfo, updateSpec, context) + if (!dialogOk) { + return + } + } + + // could be called by external plugin via com.intellij.openapi.vcs.update.AbstractCommonUpdateAction.actionPerformed + // and we have no guarantees about threading there + if (ApplicationManager.getApplication().isDispatchThread()) { + // Not only documents, but also project settings should be saved, + // to ensure that if as a result of Update some project settings will be changed, + // all local changes are saved in prior and do not overwrite remote changes. + // Also, there is a chance that save during update can break it - + // we do disable auto saving during update, but still, there is a chance that save will occur. + FileDocumentManager.getInstance().saveAllDocuments() + forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod(project, false) + } + + launchUpdate(project, roots, updateSpec, actionInfo, actionName, onSuccess) + } + + @ApiStatus.Internal + fun launchUpdate( + project: Project, + roots: Array, + updateSpec: List, + actionInfo: ActionInfo, + @Nls actionName: String, + @RequiresEdt onSuccess: () -> Unit = {}, + ) { + object : Updater(project, roots, updateSpec, actionInfo, actionName) { + override fun onSuccess() { + super.onSuccess() + onSuccess() + } + }.queue() + } + + @ApiStatus.Obsolete + @ApiStatus.Internal + @JvmStatic + fun runUpdateBlocking( + project: Project, + roots: Array, + updateSpec: List, + actionInfo: ActionInfo, + @Nls actionName: String, + ) { + Updater(project, roots, updateSpec, actionInfo, actionName).run(EmptyProgressIndicator()) + } + + @ApiStatus.Internal + @JvmStatic + fun getRoots( + project: Project, + actionInfo: ActionInfo, + scopeInfo: ScopeInfo, + context: DataContext, + filterDescending: Boolean = true, + ): Array { + val filePaths = scopeInfo.getRoots(context, actionInfo) + val filterExistsInVcs = scopeInfo.filterExistsInVcs() + val roots = filterRoots(project, filePaths, filterExistsInVcs, actionInfo::getEnvironment) + return if (filterDescending) { + DescindingFilesFilter.filterDescindingFiles(roots, project) + } + else { + roots + } + } + + private fun filterRoots( + project: Project, + roots: Collection, + filterExistsInVcs: Boolean, + updateEnvironmentSupplier: (AbstractVcs) -> UpdateEnvironment?, + ): Array { + val result = ArrayList() + for (file in roots) { + val vcs = VcsUtil.getVcsFor(project, file) ?: continue + if (!filterExistsInVcs || AbstractVcs.fileInVcsByFileStatus(project, file)) { + val updateEnvironment = updateEnvironmentSupplier(vcs) + if (updateEnvironment != null) { + result.add(file) + } + } + else { + val virtualFile = file.getVirtualFile() + if (virtualFile != null && virtualFile.isDirectory()) { + val vcsRoots = ProjectLevelVcsManager.getInstance(project).getAllVersionedRoots() + for (vcsRoot in vcsRoots) { + if (VfsUtilCore.isAncestor(virtualFile, vcsRoot, false)) { + result.add(file) + } + } + } + } + } + return result.toTypedArray() + } + + @ApiStatus.Internal + @JvmStatic + fun createUpdateSpec( + project: Project, + roots: Array, + actionInfo: ActionInfo, + ): List { + val resultPrep = mutableMapOf>() + for (file in roots) { + val vcs = VcsUtil.getVcsFor(project, file) ?: continue + resultPrep.getOrPut(vcs) { SmartHashSet() }.add(file) + } + return buildList { + for ((vcs, roots) in resultPrep.entries) { + val environment = actionInfo.getEnvironment(vcs) ?: continue + + @Suppress("DEPRECATION") + val uniqueRoots = vcs.filterUniqueRoots(roots.toList(), FilePath::getVirtualFile) + add(VcsUpdateSpecification(vcs, environment, uniqueRoots)) + } + } + } + + private fun isUpdateSpecValid(spec: List): Boolean { + for ((_, updateEnvironment, roots) in spec) { + if (!updateEnvironment.validateOptions(roots)) { + // messages already shown + return false + } + } + return true + } + + @ApiStatus.Internal + fun showOptionsDialog( + project: Project, + actionInfo: ActionInfo, + scopeInfo: ScopeInfo, + updateSpec: List, + dataContext: DataContext, + ): Boolean { + val envToConfMap = createConfigurableToEnvMap(updateSpec) + val scopeName = scopeInfo.getScopeName(dataContext, actionInfo) + if (!envToConfMap.isEmpty()) { + val dialogOrStatus = actionInfo.createOptionsDialog(project, envToConfMap, scopeName) + return dialogOrStatus.showAndGet() + } + return true + } + + private fun createConfigurableToEnvMap(updateSpec: List): LinkedHashMap { + val envToConfMap = LinkedHashMap() + for ((vcs, environment, roots) in updateSpec) { + val configurable = environment.createConfigurable(roots) + if (configurable != null) { + envToConfMap[configurable] = vcs + } + } + return envToConfMap + } + + @JvmStatic + fun checkUpdateHasCustomNotification(vcss: Collection): Boolean { + return vcss.all { vcs -> + val environment = vcs.updateEnvironment + environment != null && environment.hasCustomNotification() + } + } + + private val LOG = logger() +} + +@ApiStatus.Internal +data class VcsUpdateSpecification( + val vcs: AbstractVcs, + val environment: UpdateEnvironment, + val roots: Collection, +) \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateExecutionProcess.kt b/plugins/git4idea/src/git4idea/update/GitUpdateExecutionProcess.kt index 433061a024fe..732559842d97 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateExecutionProcess.kt +++ b/plugins/git4idea/src/git4idea/update/GitUpdateExecutionProcess.kt @@ -1,18 +1,17 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package git4idea.update +import com.intellij.openapi.components.service import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Ref -import com.intellij.openapi.vcs.AbstractVcs import com.intellij.openapi.vcs.FilePath import com.intellij.openapi.vcs.VcsNotifier import com.intellij.openapi.vcs.update.* -import com.intellij.util.containers.toArray -import com.intellij.vcsUtil.VcsUtil +import com.intellij.vcsUtil.VcsUtil.getFilePath import git4idea.GitNotificationIdsHolder.Companion.BRANCH_SET_UPSTREAM_ERROR import git4idea.GitNotificationIdsHolder.Companion.UPDATE_NOTHING_TO_UPDATE +import git4idea.GitVcs import git4idea.branch.GitBranchPair import git4idea.commands.Git import git4idea.commands.GitCommand @@ -20,7 +19,6 @@ import git4idea.commands.GitLineHandler import git4idea.config.UpdateMethod import git4idea.i18n.GitBundle import git4idea.repo.GitRepository -import git4idea.update.GitUpdateEnvironment.performUpdate internal class GitUpdateExecutionProcess( private val project: Project, @@ -37,47 +35,26 @@ internal class GitUpdateExecutionProcess( return } - val vcsToRoots = getVcsRoots(repositories) - val roots = vcsToRoots.values.flatten().toArray(emptyArray()) + val roots = repositories.map { getFilePath(it.root) } + val gitUpdateEnvironment = project.service() + val updateEnvironment = object : UpdateEnvironment by gitUpdateEnvironment { + override fun updateDirectories(contentRoots: Array, updatedFiles: UpdatedFiles, progressIndicator: ProgressIndicator, context: Ref): UpdateSession { + if (shouldSetAsUpstream) { + updateConfig.forEach { (repository, branchPair) -> setBranchUpstream(repository, branchPair) } + } - ProgressManager.getInstance() - .run(UpdateExecution( - project = project, - vcsToRoots = vcsToRoots, - roots = roots, - updateConfig = updateConfig, - updateMethod = updateMethod, - shouldSetAsUpstream = shouldSetAsUpstream, - )) - } + return GitUpdateEnvironment.performUpdate(project, contentRoots, updatedFiles, progressIndicator, updateMethod, updateConfig) + } - private fun getVcsRoots(repositories: Collection): Map> { - return repositories.associate { repo -> - repo.vcs to listOf(VcsUtil.getFilePath(repo.root)) + override fun hasCustomNotification(): Boolean = gitUpdateEnvironment.hasCustomNotification() } - } -} + val spec = VcsUpdateSpecification(GitVcs.getInstance(project), updateEnvironment, roots) -private class UpdateExecution( - project: Project, - vcsToRoots: Map>, - private val roots: Array, - private val updateConfig: Map, - private val updateMethod: UpdateMethod, - private val shouldSetAsUpstream: Boolean = false, -) - : AbstractCommonUpdateAction.Updater(project, roots, vcsToRoots, ActionInfo.UPDATE, GitBundle.message("progress.title.update")) { - override fun performUpdate( - progressIndicator: ProgressIndicator?, - updateEnvironment: UpdateEnvironment, - files: Collection, - refContext: Ref, - ): UpdateSession { - if (shouldSetAsUpstream) { - updateConfig.forEach { (repository, branchPair) -> setBranchUpstream(repository, branchPair) } - } - - return performUpdate(project, roots, updatedFiles, progressIndicator, updateMethod, updateConfig) + VcsUpdateProcess.launchUpdate(project, + roots.toTypedArray(), + listOf(spec), + ActionInfo.UPDATE, + GitBundle.message("progress.title.update")) } private fun setBranchUpstream(repository: GitRepository, branchConfig: GitBranchPair) { diff --git a/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnTestCase.java b/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnTestCase.java index a3a3d23f7ca2..dd94089e1f46 100644 --- a/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnTestCase.java +++ b/plugins/svn4idea/testSource/org/jetbrains/idea/svn/SvnTestCase.java @@ -2,9 +2,6 @@ package org.jetbrains.idea.svn; import com.intellij.execution.process.ProcessOutput; -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.impl.SimpleDataContext; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.command.undo.UndoManager; @@ -16,7 +13,9 @@ import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; import com.intellij.openapi.vcs.rollback.RollbackProgressListener; -import com.intellij.openapi.vcs.update.CommonUpdateProjectAction; +import com.intellij.openapi.vcs.update.ActionInfo; +import com.intellij.openapi.vcs.update.ScopeInfo; +import com.intellij.openapi.vcs.update.VcsUpdateProcess; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.testFramework.ApplicationRule; @@ -440,11 +439,13 @@ public abstract class SvnTestCase extends AbstractJunitVcsTestCase { protected void imitUpdate() { vcsManager.getOptions(VcsConfiguration.StandardOption.UPDATE).setValue(false); - final CommonUpdateProjectAction action = new CommonUpdateProjectAction(); - action.getTemplatePresentation().setText("1"); - action - .actionPerformed(new AnActionEvent(null, SimpleDataContext.getProjectContext(myProject), "test", new Presentation(), ActionManager.getInstance(), 0)); + var actionInfo = ActionInfo.UPDATE; + var scopeInfo = ScopeInfo.PROJECT; + var context = SimpleDataContext.getProjectContext(myProject); + var roots = VcsUpdateProcess.getRoots(myProject, actionInfo, scopeInfo, context, true); + var updateSpec = VcsUpdateProcess.createUpdateSpec(myProject, roots, actionInfo); + VcsUpdateProcess.runUpdateBlocking(myProject, roots, updateSpec, actionInfo, "1"); waitChangesAndAnnotations(); }