[vcs] don't execute action manually just to update the project

Extract the functionality to a separate file and mark actions impl internal

GitOrigin-RevId: e41f7a45168c316144bd04912905e94a6bd9ddd1
This commit is contained in:
Ivan Semenov
2025-08-12 13:43:53 +00:00
committed by intellij-monorepo-bot
parent a2889d758c
commit bfee77d859
8 changed files with 326 additions and 283 deletions
@@ -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)
}
}
}
@@ -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
-18
View File
@@ -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:<init>(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
- <init>():V
- p:filterRootsBeforeAction():Z
com.intellij.openapi.vcs.update.FilePathChange
- a:getAfterPath():com.intellij.openapi.vcs.FilePath
- a:getBeforePath():com.intellij.openapi.vcs.FilePath
@@ -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<AbstractVcs>): Boolean {
return vcss.all { vcs ->
val environment = vcs.updateEnvironment
environment != null && environment.hasCustomNotification()
}
}
fun showsCustomNotification(vcss: Collection<AbstractVcs>): 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<AbstractVcs, Collection<FilePath>>, 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<FilePath> {
val filePaths = scopeInfo.getRoots(context, actionInfo)
return DescindingFilesFilter.filterDescindingFiles(filterRoots(project, filePaths), project)
}
private fun createConfigurableToEnvMap(updateEnvToVirtualFiles: Map<AbstractVcs, Collection<FilePath>>): LinkedHashMap<Configurable, AbstractVcs> {
val envToConfMap = LinkedHashMap<Configurable, AbstractVcs>()
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<Configurable, AbstractVcs> {
val roots = getRoots(project, SimpleDataContext.getProjectContext(project))
val vcsToFilesMap = createVcsToFilesMap(roots, project)
return createConfigurableToEnvMap(vcsToFilesMap)
}
private fun createVcsToFilesMap(roots: Array<FilePath>, project: Project): Map<AbstractVcs, Collection<FilePath>> {
val resultPrep = MultiMap.createSet<AbstractVcs, FilePath>()
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<AbstractVcs, MutableCollection<FilePath>>()
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<FilePath>): Array<FilePath> {
val result = ArrayList<FilePath>()
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<FilePath>,
private val vcsToVirtualFiles: Map<AbstractVcs, Collection<FilePath>>,
private val spec: List<VcsUpdateSpecification>,
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<SequentialUpdatesContext>(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<FilePath>,
refContext: Ref<SequentialUpdatesContext>
): UpdateSession {
return updateEnvironment.updateDirectories(files.toTypedArray(), updatedFiles, progressIndicator, refContext)
}
private fun gatherExceptions(vcs: AbstractVcs, exceptionList: MutableList<VcsException>) {
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<UpdateSession>
updateSessions: List<UpdateSession>,
): 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<AbstractVcs>): Boolean {
private fun canGroupByChangelist(vcses: Collection<AbstractVcs>): 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<UpdateSession>): Boolean = updateSessions.any { it.isCanceled() }
private fun someSessionWasCanceled(updateSessions: List<UpdateSession>): Boolean = updateSessions.any { it.isCanceled() }
private fun getAllFilesAreUpToDateMessage(roots: Array<FilePath>): @NlsContexts.NotificationContent String {
if (roots.size == 1 && !roots[0].isDirectory()) {
@@ -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);
@@ -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<FilePath>,
updateSpec: List<VcsUpdateSpecification>,
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<FilePath>,
updateSpec: List<VcsUpdateSpecification>,
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<FilePath> {
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<FilePath>,
filterExistsInVcs: Boolean,
updateEnvironmentSupplier: (AbstractVcs) -> UpdateEnvironment?,
): Array<FilePath> {
val result = ArrayList<FilePath>()
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<FilePath>,
actionInfo: ActionInfo,
): List<VcsUpdateSpecification> {
val resultPrep = mutableMapOf<AbstractVcs, MutableCollection<FilePath>>()
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<VcsUpdateSpecification>): 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<VcsUpdateSpecification>,
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<VcsUpdateSpecification>): LinkedHashMap<Configurable, AbstractVcs> {
val envToConfMap = LinkedHashMap<Configurable, AbstractVcs>()
for ((vcs, environment, roots) in updateSpec) {
val configurable = environment.createConfigurable(roots)
if (configurable != null) {
envToConfMap[configurable] = vcs
}
}
return envToConfMap
}
@JvmStatic
fun checkUpdateHasCustomNotification(vcss: Collection<AbstractVcs>): Boolean {
return vcss.all { vcs ->
val environment = vcs.updateEnvironment
environment != null && environment.hasCustomNotification()
}
}
private val LOG = logger<VcsUpdateProcess>()
}
@ApiStatus.Internal
data class VcsUpdateSpecification(
val vcs: AbstractVcs,
val environment: UpdateEnvironment,
val roots: Collection<FilePath>,
)
@@ -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<GitUpdateEnvironment>()
val updateEnvironment = object : UpdateEnvironment by gitUpdateEnvironment {
override fun updateDirectories(contentRoots: Array<out FilePath>, updatedFiles: UpdatedFiles, progressIndicator: ProgressIndicator, context: Ref<SequentialUpdatesContext?>): 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<GitRepository>): Map<AbstractVcs, Collection<FilePath>> {
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<AbstractVcs, Collection<FilePath>>,
private val roots: Array<FilePath>,
private val updateConfig: Map<GitRepository, GitBranchPair>,
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<FilePath>,
refContext: Ref<SequentialUpdatesContext>,
): 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) {
@@ -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();
}