diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ignore/IgnoredToExcludedSynchronizer.kt b/platform/dvcs-impl/src/com/intellij/dvcs/ignore/IgnoredToExcludedSynchronizer.kt index f0eade5289d2..b0678ecc9baf 100644 --- a/platform/dvcs-impl/src/com/intellij/dvcs/ignore/IgnoredToExcludedSynchronizer.kt +++ b/platform/dvcs-impl/src/com/intellij/dvcs/ignore/IgnoredToExcludedSynchronizer.kt @@ -9,6 +9,7 @@ import com.intellij.ide.util.PropertiesComponent import com.intellij.idea.ActionsBundle import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.Service @@ -16,10 +17,8 @@ import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.runModalTask import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project @@ -41,17 +40,17 @@ import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog import com.intellij.openapi.vcs.ignore.IgnoredToExcludedSynchronizerConstants.ASKED_MARK_IGNORED_FILES_AS_EXCLUDED_PROPERTY import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.backend.workspace.WorkspaceModel +import com.intellij.platform.ide.progress.withModalProgress import com.intellij.platform.workspace.jps.entities.ContentRootEntity import com.intellij.platform.workspace.jps.entities.SourceRootEntity import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider import com.intellij.ui.EditorNotifications import com.intellij.util.Alarm -import com.intellij.util.application import com.intellij.util.ui.update.MergingUpdateQueue import com.intellij.util.ui.update.Update -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex import org.jetbrains.annotations.ApiStatus import java.util.* import java.util.function.Function @@ -59,14 +58,6 @@ import javax.swing.JComponent private val LOG = logger() -private fun exclude(module: Module, dirs: Collection) { - runInEdt { - MarkRootsManager.modifyRoots(module, dirs.toTypedArray()) { vFile, entry -> - entry.addExcludeFolder(vFile) - } - } -} - /** * Shows [EditorNotifications] in .ignore files with a suggestion to exclude ignored directories. * Silently excludes them if [VcsConfiguration.MARK_IGNORED_AS_EXCLUDED] is enabled. @@ -74,7 +65,7 @@ private fun exclude(module: Module, dirs: Collection) { * Not internal service. Can be used directly in related modules. */ @Service(Service.Level.PROJECT) -class IgnoredToExcludedSynchronizer(project: Project, cs: CoroutineScope) : FilesProcessorImpl(project, project) { +class IgnoredToExcludedSynchronizer(project: Project, private val cs: CoroutineScope) : FilesProcessorImpl(project, project) { private val queue = MergingUpdateQueue("IgnoredToExcludedSynchronizer", 1000, true, null, this, null, Alarm.ThreadToUse.POOLED_THREAD) init { @@ -173,6 +164,43 @@ class IgnoredToExcludedSynchronizer(project: Project, cs: CoroutineScope) : File doActionOnChosenFiles(doFilterFiles(ignoredDirs)) } } + + private val ignoredToExcludeCheckMutex = Mutex() + + internal fun checkIgnoredToExclude() { + cs.launch(Dispatchers.Default, CoroutineStart.UNDISPATCHED) { + // shouldn't be run in parallel + val canRun = ignoredToExcludeCheckMutex.tryLock() + if (!canRun) { + return@launch + } + yield() + + try { + val dirsToExclude = withModalProgress(project, ActionsBundle.message("action.CheckIgnoredAndNotExcludedDirectories.progress")) { + VcsIgnoreManagerImpl.getInstanceImpl(project).awaitRefreshQueue() + val ignoredFilePaths = ChangeListManager.getInstance(project).ignoredFilePaths + determineIgnoredDirsToExclude(project, ignoredFilePaths) + } + + if (dirsToExclude.isEmpty()) { + VcsNotifier.getInstance(project) + .notifyMinorInfo(IGNORED_TO_EXCLUDE_NOT_FOUND, "", message("ignore.to.exclude.no.directories.found")) + } + else { + val userSelectedFiles = withContext(Dispatchers.EDT) { + selectFilesToExclude(project, dirsToExclude) + } + if (userSelectedFiles.isNotEmpty()) { + markIgnoredAsExcluded(project, userSelectedFiles) + } + } + } + finally { + ignoredToExcludeCheckMutex.unlock() + } + } + } } private fun markIgnoredAsExcluded(project: Project, files: Collection) { @@ -184,7 +212,11 @@ private fun markIgnoredAsExcluded(project: Project, files: Collection()) { vFile, entry -> + entry.addExcludeFolder(vFile) + } + } } } @@ -280,25 +312,8 @@ internal class CheckIgnoredToExcludeAction : DumbAwareAction() { } override fun actionPerformed(e: AnActionEvent) { - val project = e.project!! - - runModalTask(ActionsBundle.message("action.CheckIgnoredAndNotExcludedDirectories.progress"), project, true) { - VcsIgnoreManagerImpl.getInstanceImpl(project).awaitRefreshQueue() - val ignoredFilePaths = ChangeListManager.getInstance(project).ignoredFilePaths - val dirsToExclude = determineIgnoredDirsToExclude(project, ignoredFilePaths) - if (dirsToExclude.isEmpty()) { - VcsNotifier.getInstance(project) - .notifyMinorInfo(IGNORED_TO_EXCLUDE_NOT_FOUND, "", message("ignore.to.exclude.no.directories.found")) - } - else { - application.invokeAndWait { - val userSelectedFiles = selectFilesToExclude(project, dirsToExclude) - if (userSelectedFiles.isNotEmpty()) { - markIgnoredAsExcluded(project, userSelectedFiles) - } - } - } - } + val project = e.project ?: return + project.service().checkIgnoredToExclude() } } diff --git a/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.kt b/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.kt index 0532965ec803..8fdf4a8b14c2 100644 --- a/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.kt +++ b/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.kt @@ -48,6 +48,7 @@ class GitUntrackedFilesHolder internal constructor( private val repoRoot: VirtualFile = repository.getRoot() private val dirtyFiles = HashSet() + @Volatile private var isEverythingDirty = true @Volatile @@ -205,27 +206,21 @@ class GitUntrackedFilesHolder internal constructor( * Queries Git to check the status of [dirtyFiles] and moves them to [untrackedFiles]. */ private suspend fun doUpdate() { - val (nothingToDo, dirt) = acquireDirt() - if (nothingToDo) { + val dirtyScope = acquireDirt() + if (dirtyScope == null) { BackgroundTaskUtil.syncPublisher(project, VcsManagedFilesHolder.TOPIC).updatingModeChanged() return } BackgroundTaskUtil.syncPublisher(project, GitRefreshListener.TOPIC).progressStarted() try { - val everythingDirty = dirt == null || dirt.contains(VcsUtil.getFilePath(repoRoot)) - val activity = logUntrackedRefresh(project, everythingDirty) - val (untracked, ignored) = refreshFiles(dirt) + val activity = logUntrackedRefresh(project, dirtyScope == DirtyScope.Everything) + val (untracked, ignored) = refreshFiles(dirtyScope) activity.finished() val filteredUntracked = removePathsUnderOtherRoots(untracked, "unversioned") val filteredIgnored = removePathsUnderOtherRoots(ignored, "ignored") - val dirtyScope = dirt?.let { - RecursiveFilePathSet(repoRoot.isCaseSensitive).apply { - addAll(it) - } - } val (oldIgnored, newIgnored) = applyRefreshResult(filteredUntracked, filteredIgnored, dirtyScope) BackgroundTaskUtil.syncPublisher(project, GitRefreshListener.TOPIC).repositoryUpdated(repository) @@ -239,55 +234,72 @@ class GitUntrackedFilesHolder internal constructor( } } - private fun acquireDirt(): Pair?> { - synchronized(LOCK) { - val nothingToDo = !isDirty - if (nothingToDo) isInUpdateMode = false - - val dirt = if (isEverythingDirty) null else dirtyFiles.toList() - dirtyFiles.clear() - isEverythingDirty = false - return nothingToDo to dirt + private fun acquireDirt(): DirtyScope? { + return synchronized(LOCK) { + try { + when { + isEverythingDirty || dirtyFiles.contains(VcsUtil.getFilePath(repoRoot)) -> { + DirtyScope.Everything + } + dirtyFiles.isNotEmpty() -> { + DirtyScope.Files(dirtyFiles.toList()) + } + else -> { + isInUpdateMode = false + null + } + } + } + finally { + dirtyFiles.clear() + isEverythingDirty = false + } } } private fun applyRefreshResult( untracked: Set, ignored: Set, - dirtyScope: RecursiveFilePathSet?, - ): Pair, Set> { + dirtyScope: DirtyScope, + ): UpdatedValue> { synchronized(LOCK) { val oldIgnored = _ignoredFilesHolder.ignoredFilePaths - val newIgnored = RecursiveFilePathSet(repoRoot.isCaseSensitive) - val newUntracked = RecursiveFilePathSet(repoRoot.isCaseSensitive) + val caseSensitive = repoRoot.isCaseSensitive + val newIgnored = RecursiveFilePathSet(caseSensitive) + val newUntracked = RecursiveFilePathSet(caseSensitive) - if (dirtyScope != null) { - val untrackedSet = untrackedFiles.toSet() - untrackedSet.removeIf { dirtyScope.hasAncestor(it) } - untrackedSet.addAll(untracked) - newUntracked.addAll(untrackedSet) + when (dirtyScope) { + DirtyScope.Everything -> { + newUntracked.addAll(untracked) + newIgnored.addAll(ignored) + } + is DirtyScope.Files -> { + val dirtyFiles = RecursiveFilePathSet(caseSensitive).apply { + addAll(dirtyScope.files) + } + val untrackedSet = untrackedFiles.toSet() + untrackedSet.removeIf { dirtyFiles.hasAncestor(it) } + untrackedSet.addAll(untracked) + newUntracked.addAll(untrackedSet) - for (filePath in oldIgnored) { - if (!dirtyScope.hasAncestor(filePath)) { - newIgnored.add(filePath) + for (filePath in oldIgnored) { + if (!dirtyFiles.hasAncestor(filePath)) { + newIgnored.add(filePath) + } + } + for (filePath in ignored) { + if (!newIgnored.hasAncestor(filePath)) { // prevent storing both parent and child directories + newIgnored.add(filePath) + } } } - for (filePath in ignored) { - if (!newIgnored.hasAncestor(filePath)) { // prevent storing both parent and child directories - newIgnored.add(filePath) - } - } - } - else { - newUntracked.addAll(untracked) - newIgnored.addAll(ignored) } _ignoredFilesHolder.ignoredFiles.set(newIgnored) untrackedFiles.set(newUntracked) isInUpdateMode = isDirty - return oldIgnored to _ignoredFilesHolder.ignoredFilePaths + return UpdatedValue(oldIgnored, _ignoredFilesHolder.ignoredFilePaths) } } @@ -322,12 +334,16 @@ class GitUntrackedFilesHolder internal constructor( } - private suspend fun refreshFiles(dirty: List?): RefreshResult { + private suspend fun refreshFiles(dirtyScope: DirtyScope): RefreshResult { try { val withIgnored = AdvancedSettings.getBoolean("vcs.process.ignored") val fileStatuses = withContext(Dispatchers.IO) { + val dirtyFiles = when (dirtyScope) { + DirtyScope.Everything -> emptyList() + is DirtyScope.Files -> dirtyScope.files + } coroutineToIndicator { - getFileStatus(project, repoRoot, dirty.orEmpty(), false, true, withIgnored) + getFileStatus(project, repoRoot, dirtyFiles, false, true, withIgnored) } } @@ -409,7 +425,14 @@ class GitUntrackedFilesHolder internal constructor( } } +private sealed interface DirtyScope { + class Files(val files: List) : DirtyScope + object Everything : DirtyScope +} + private data class RefreshResult( val untracked: Set = emptySet(), val ignored: Set = emptySet(), ) + +private data class UpdatedValue(val old: T, val new: T)