[vcs] run ignored to excluded check under a coroutine

GitOrigin-RevId: 497999ad700156d64a9145025a3d65c9bc0eaba1
This commit is contained in:
Ivan Semenov
2025-08-18 11:20:44 +00:00
committed by intellij-monorepo-bot
parent 76ba594010
commit bda5729f05
2 changed files with 115 additions and 77 deletions
@@ -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<IgnoredToExcludedSynchronizer>()
private fun exclude(module: Module, dirs: Collection<VirtualFile>) {
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<VirtualFile>) {
* 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<VirtualFile>) {
@@ -184,7 +212,11 @@ private fun markIgnoredAsExcluded(project: Project, files: Collection<VirtualFil
}
for ((module, ignoredDirs) in ignoredDirsByModule) {
exclude(module!!, ignoredDirs)
runInEdt {
MarkRootsManager.modifyRoots(module!!, ignoredDirs.toTypedArray<VirtualFile>()) { 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<IgnoredToExcludedSynchronizer>().checkIgnoredToExclude()
}
}
@@ -48,6 +48,7 @@ class GitUntrackedFilesHolder internal constructor(
private val repoRoot: VirtualFile = repository.getRoot()
private val dirtyFiles = HashSet<FilePath>()
@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<Boolean, List<FilePath>?> {
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<FilePath>,
ignored: Set<FilePath>,
dirtyScope: RecursiveFilePathSet?,
): Pair<Set<FilePath>, Set<FilePath>> {
dirtyScope: DirtyScope,
): UpdatedValue<Set<FilePath>> {
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<FilePath>?): 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<FilePath>) : DirtyScope
object Everything : DirtyScope
}
private data class RefreshResult(
val untracked: Set<FilePath> = emptySet(),
val ignored: Set<FilePath> = emptySet(),
)
private data class UpdatedValue<T>(val old: T, val new: T)