diff --git a/plugins/git4idea/resources/META-INF/stage-plugin.xml b/plugins/git4idea/resources/META-INF/stage-plugin.xml
index 2585f326ce58..4a63ed5dba0d 100644
--- a/plugins/git4idea/resources/META-INF/stage-plugin.xml
+++ b/plugins/git4idea/resources/META-INF/stage-plugin.xml
@@ -5,6 +5,10 @@
displayNameSupplierClassName="git4idea.index.GitStageDisplayNameSupplier"/>
+
+
+
+
diff --git a/plugins/git4idea/src/git4idea/index/GitIndexUtil.java b/plugins/git4idea/src/git4idea/index/GitIndexUtil.java
index 1310d6d29928..b2d34489f66d 100644
--- a/plugins/git4idea/src/git4idea/index/GitIndexUtil.java
+++ b/plugins/git4idea/src/git4idea/index/GitIndexUtil.java
@@ -59,11 +59,20 @@ public class GitIndexUtil {
}
@NotNull
- public static List listStaged(@NotNull GitRepository repository, @NotNull Collection extends FilePath> filePaths) throws VcsException {
- List result = new ArrayList<>();
+ public static List listStaged(@NotNull GitRepository repository, @NotNull Collection extends FilePath> filePaths)
+ throws VcsException {
+ Project project = repository.getProject();
VirtualFile root = repository.getRoot();
- GitLineHandler h = new GitLineHandler(repository.getProject(), root, GitCommand.LS_FILES);
+ return listStaged(project, root, filePaths);
+ }
+
+ @NotNull
+ public static List listStaged(@NotNull Project project,
+ @NotNull VirtualFile root,
+ @NotNull Collection extends FilePath> filePaths) throws VcsException {
+ List result = new ArrayList<>();
+ GitLineHandler h = new GitLineHandler(project, root, GitCommand.LS_FILES);
h.addParameters("-s");
h.endOptions();
h.addRelativePaths(filePaths);
@@ -176,20 +185,24 @@ public class GitIndexUtil {
}
@NotNull
- public static Hash write(@NotNull GitRepository repository,
- @NotNull FilePath filePath,
- @NotNull InputStream content,
+ public static Hash write(@NotNull GitRepository repository, @NotNull FilePath filePath, @NotNull InputStream content,
boolean executable) throws VcsException {
- Hash hash = hashObject(repository, filePath, content);
- updateIndex(repository, filePath, hash, executable);
+ return write(repository.getProject(), repository.getRoot(), filePath, content, executable);
+ }
+
+ @NotNull
+ public static Hash write(@NotNull Project project, @NotNull VirtualFile root,
+ @NotNull FilePath filePath, @NotNull InputStream content,
+ boolean executable) throws VcsException {
+ Hash hash = hashObject(project, root, filePath, content);
+ updateIndex(project, root, filePath, hash, executable);
return hash;
}
@NotNull
- private static Hash hashObject(@NotNull GitRepository repository,
- @NotNull FilePath filePath,
+ private static Hash hashObject(@NotNull Project project, @NotNull VirtualFile root, @NotNull FilePath filePath,
@NotNull InputStream content) throws VcsException {
- GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.HASH_OBJECT);
+ GitLineHandler h = new GitLineHandler(project, root, GitCommand.HASH_OBJECT);
h.setSilent(true);
h.addParameters("-w", "--stdin");
h.addParameters("--path");
@@ -204,11 +217,17 @@ public class GitIndexUtil {
@NotNull FilePath filePath,
@NotNull Hash blobHash,
boolean isExecutable) throws VcsException {
- String mode = isExecutable ? EXECUTABLE_MODE : DEFAULT_MODE;
- String path = VcsFileUtil.relativePath(repository.getRoot(), filePath);
+ updateIndex(repository.getProject(), repository.getRoot(), filePath, blobHash, isExecutable);
+ }
- GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.UPDATE_INDEX);
- if (GitVersionSpecialty.CACHEINFO_SUPPORTS_SINGLE_PARAMETER_FORM.existsIn(repository)) {
+ public static void updateIndex(@NotNull Project project, @NotNull VirtualFile root, @NotNull FilePath filePath,
+ @NotNull Hash blobHash,
+ boolean isExecutable) throws VcsException {
+ String mode = isExecutable ? EXECUTABLE_MODE : DEFAULT_MODE;
+ String path = VcsFileUtil.relativePath(root, filePath);
+
+ GitLineHandler h = new GitLineHandler(project, root, GitCommand.UPDATE_INDEX);
+ if (GitVersionSpecialty.CACHEINFO_SUPPORTS_SINGLE_PARAMETER_FORM.existsIn(project)) {
h.addParameters("--cacheinfo", mode + "," + blobHash.asString() + "," + path);
}
else {
diff --git a/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystem.kt b/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystem.kt
new file mode 100644
index 000000000000..a5f54a2d7621
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystem.kt
@@ -0,0 +1,56 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.index.vfs
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileListener
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.VirtualFileSystem
+import com.intellij.openapi.vfs.newvfs.VirtualFileFilteringListener
+import com.intellij.util.containers.ContainerUtil
+
+private const val PROTOCOL = "gitIndexFs"
+
+class GitIndexFileSystem : VirtualFileSystem() {
+ private val listenerWrappers: MutableMap = ContainerUtil.newConcurrentMap()
+
+ override fun getProtocol(): String = PROTOCOL
+ override fun isReadOnly(): Boolean = false
+
+ override fun findFileByPath(path: String): VirtualFile? {
+ val (project, virtualFile, filePath) = GitIndexVirtualFile.decode(path) ?: return null
+
+ project.service() // init service
+ return project.service().get(virtualFile, filePath)
+ }
+
+ override fun refreshAndFindFileByPath(path: String): VirtualFile? = findFileByPath(path)
+ override fun refresh(asynchronous: Boolean) {}
+
+ override fun extractPresentableUrl(path: String): String = "Staged: " + super.extractPresentableUrl(path)
+
+ override fun addVirtualFileListener(listener: VirtualFileListener) {
+ val wrapper: VirtualFileListener = VirtualFileFilteringListener(listener, this)
+ VirtualFileManager.getInstance().addVirtualFileListener(wrapper)
+ listenerWrappers[listener] = wrapper
+ }
+
+ override fun removeVirtualFileListener(listener: VirtualFileListener) {
+ listenerWrappers.remove(listener)?.let {
+ VirtualFileManager.getInstance().removeVirtualFileListener(it)
+ }
+ }
+
+ override fun deleteFile(requestor: Any?, vFile: VirtualFile) = throw UnsupportedOperationException()
+ override fun moveFile(requestor: Any?, vFile: VirtualFile, newParent: VirtualFile) = throw UnsupportedOperationException()
+ override fun renameFile(requestor: Any?, vFile: VirtualFile, newName: String) = throw UnsupportedOperationException()
+ override fun createChildFile(requestor: Any?, vDir: VirtualFile, fileName: String): VirtualFile = throw UnsupportedOperationException()
+ override fun createChildDirectory(requestor: Any?, vDir: VirtualFile, dirName: String): VirtualFile = throw UnsupportedOperationException()
+ override fun copyFile(requestor: Any?, virtualFile: VirtualFile, newParent: VirtualFile, copyName: String): VirtualFile = throw UnsupportedOperationException()
+
+ companion object {
+ @JvmStatic
+ val instance: GitIndexFileSystem
+ get() = VirtualFileManager.getInstance().getFileSystem(PROTOCOL) as GitIndexFileSystem
+ }
+}
\ No newline at end of file
diff --git a/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystemRefresher.kt b/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystemRefresher.kt
new file mode 100644
index 000000000000..7755826b0ca8
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/index/vfs/GitIndexFileSystemRefresher.kt
@@ -0,0 +1,101 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.index.vfs
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.serviceIfCreated
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.newvfs.BulkFileListener
+import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent
+import com.intellij.util.concurrency.AppExecutorUtil
+import com.intellij.util.messages.MessageBusConnection
+import com.intellij.vcs.log.submitSafe
+import git4idea.repo.GitRepositoryManager
+import git4idea.repo.GitUntrackedFilesHolder
+import org.jetbrains.annotations.CalledWithWriteLock
+
+class GitIndexFileSystemRefresher(private val project: Project) : Disposable {
+ private val executor = AppExecutorUtil.createBoundedApplicationPoolExecutor("Git Index Read/Write Thread for " + project.name, 1)
+
+ init {
+ val connection: MessageBusConnection = project.messageBus.connect(this)
+ connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
+ override fun after(events: List) {
+ val roots = GitRepositoryManager.getInstance(project).repositories.filter { repo ->
+ events.any { e -> GitUntrackedFilesHolder.indexChanged(repo, e.path) }
+ }.map { it.root }
+ if (roots.isNotEmpty()) {
+ LOG.debug("Scheduling refresh for ${roots.joinToString { it.name }}")
+ refresh(roots)
+ }
+ }
+ })
+ }
+
+ private fun refresh(roots: List) {
+ executor.submitSafe(LOG) {
+ val refreshRunnables = mutableListOf()
+ project.serviceIfCreated()?.forEachFile { file ->
+ if (roots.contains(file.root)) {
+ file.getRefresh()?.let { refreshRunnables.add(it) }
+ LOG.debug("Preparing refresh for $file")
+ }
+ }
+ if (refreshRunnables.isNotEmpty()) execute(refreshRunnables)
+ }
+ }
+
+ internal fun execute(refreshList: List, postRunnable: Runnable? = null) {
+ val events = refreshList.map { it.event }
+ writeInEdt {
+ ApplicationManager.getApplication().messageBus.syncPublisher(VirtualFileManager.VFS_CHANGES).before(events)
+ refreshList.forEach { it.run() }
+ ApplicationManager.getApplication().messageBus.syncPublisher(VirtualFileManager.VFS_CHANGES).after(events)
+ postRunnable?.run()
+ }
+ }
+
+ internal fun runTask(asynchronous: Boolean, task: () -> Unit) {
+ if (ApplicationManager.getApplication().isDispatchThread || asynchronous) {
+ executor.submitSafe(LOG, task)
+ }
+ else {
+ task()
+ }
+ }
+
+ @CalledWithWriteLock
+ internal fun changeContent(file: GitIndexVirtualFile, requestor: Any?, modificationStamp: Long, writeCommand: () -> Unit) {
+ ApplicationManager.getApplication().assertWriteAccessAllowed()
+
+ val event = VFileContentChangeEvent(requestor, file, file.modificationStamp, modificationStamp, false)
+ ApplicationManager.getApplication().messageBus.syncPublisher(VirtualFileManager.VFS_CHANGES).before(listOf(event))
+ executor.submitSafe(LOG) {
+ try {
+ writeCommand()
+ }
+ finally {
+ writeInEdt { ApplicationManager.getApplication().messageBus.syncPublisher(VirtualFileManager.VFS_CHANGES).after(listOf(event)) }
+ }
+ }
+ }
+
+ override fun dispose() {
+ }
+
+ companion object {
+ private val LOG = Logger.getInstance(GitIndexFileSystemRefresher::class.java)
+ }
+}
+
+fun writeInEdt(action: () -> Unit) {
+ ApplicationManager.getApplication().invokeLater {
+ ApplicationManager.getApplication().runWriteAction {
+ action()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFile.kt b/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFile.kt
new file mode 100644
index 000000000000..79e76b356064
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFile.kt
@@ -0,0 +1,206 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.index.vfs
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.ProjectManager
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vcs.FilePath
+import com.intellij.openapi.vcs.VcsException
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VfsUtilCore
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
+import com.intellij.util.LocalTimeCounter
+import com.intellij.vcs.log.Hash
+import com.intellij.vcs.log.impl.HashImpl
+import com.intellij.vcsUtil.VcsFileUtil
+import com.intellij.vcsUtil.VcsUtil
+import git4idea.commands.Git
+import git4idea.commands.GitCommand
+import git4idea.commands.GitLineHandler
+import git4idea.index.GitIndexUtil
+import git4idea.util.GitFileUtils
+import java.io.*
+import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
+import kotlin.concurrent.write
+
+class GitIndexVirtualFile(private val project: Project,
+ val root: VirtualFile,
+ val filePath: FilePath) : VirtualFile() {
+ private val lock = ReentrantReadWriteLock()
+ private val refresher: GitIndexFileSystemRefresher get() = project.service()
+
+ private val cachedData: AtomicReference = AtomicReference()
+
+ @Volatile
+ private var modificationStamp = LocalTimeCounter.currentTime()
+
+ init {
+ refresher.runTask(false) {
+ cachedData.compareAndSet(null, readCachedData())
+ }
+ }
+
+ override fun getFileSystem(): GitIndexFileSystem = GitIndexFileSystem.instance
+ override fun getParent(): VirtualFile? = null
+ override fun getChildren(): Array = EMPTY_ARRAY
+ override fun isWritable(): Boolean = true
+ override fun isDirectory(): Boolean = false
+ override fun isValid(): Boolean = cachedData.get() != null
+ override fun getName(): String = filePath.name
+ override fun getPath(): String = encode(project, root, filePath)
+ override fun getLength(): Long = cachedData.get()?.length ?: 0
+ override fun getTimeStamp(): Long = 0
+ override fun getModificationStamp(): Long = modificationStamp
+ override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) {
+ refresher.runTask(asynchronous) {
+ getRefresh()?.also { refresher.execute(listOf(it), postRunnable) } ?: writeInEdt { postRunnable?.run() }
+ }
+ }
+
+ internal fun getRefresh(): Refresh? {
+ val newCachedData = readCachedData()
+ val oldCachedData = cachedData.get()
+ if (oldCachedData != newCachedData) {
+ LOG.debug("Preparing refresh for $this")
+ return Refresh(oldCachedData, newCachedData, modificationStamp)
+ }
+ return null
+ }
+
+ @Throws(IOException::class)
+ override fun getOutputStream(requestor: Any?,
+ newModificationStamp: Long,
+ newTimeStamp: Long): OutputStream {
+ val outputStream: ByteArrayOutputStream = object : ByteArrayOutputStream() {
+ override fun close() = write(requestor, toByteArray(), newModificationStamp)
+ }
+ return VfsUtilCore.outputStreamAddingBOM(outputStream, this)
+ }
+
+ private fun write(requestor: Any?, newContent: ByteArray, newModificationStamp: Long) {
+ val newModStamp = if (newModificationStamp > 0) newModificationStamp else LocalTimeCounter.currentTime()
+ refresher.changeContent(this, requestor, modificationStamp) {
+ lock.write {
+ val oldCachedData = cachedData.get()
+ if (oldCachedData != readCachedData()) {
+ // TODO
+ LOG.warn("Skipping write for $this as it is not up to date")
+ return@write
+ }
+
+ val isExecutable = oldCachedData?.isExecutable ?: false
+ val newHash = GitIndexUtil.write(project, root, filePath, ByteArrayInputStream(newContent), isExecutable)
+ LOG.debug("Written $this. newHash=$newHash")
+
+ modificationStamp = newModStamp
+ if (oldCachedData?.hash != newHash) {
+ cachedData.compareAndSet(oldCachedData, CachedData(newHash, calculateLength(newHash.asString()), isExecutable))
+ }
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun getInputStream(): InputStream {
+ return VfsUtilCore.byteStreamSkippingBOM(contentsToByteArray(), this)
+ }
+
+ @Throws(IOException::class)
+ override fun contentsToByteArray(): ByteArray {
+ return try {
+ lock.read {
+ GitFileUtils.getFileContent(project, root, "", VcsFileUtil.relativePath(root, filePath))
+ }
+ }
+ catch (e: VcsException) {
+ throw IOException(e)
+ }
+ }
+
+ private fun readCachedData(): CachedData? {
+ lock.read {
+ val stagedFile = GitIndexUtil.listStaged(project, root, listOf(filePath)).singleOrNull() ?: return null
+ return CachedData(HashImpl.build(stagedFile.blobHash), calculateLength(stagedFile.blobHash), stagedFile.isExecutable)
+ }
+ }
+
+ @Throws(VcsException::class)
+ private fun calculateLength(hash: String): Long {
+ val h = GitLineHandler(project, root, GitCommand.CAT_FILE)
+ h.setSilent(true)
+ h.addParameters("-s")
+ h.addParameters(hash)
+ h.endOptions()
+ val output = Git.getInstance().runCommand(h).getOutputOrThrow()
+ return java.lang.Long.valueOf(output.trim())
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GitIndexVirtualFile
+
+ if (root != other.root) return false
+ if (filePath != other.filePath) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = root.hashCode()
+ result = 31 * result + filePath.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "GitIndexVirtualFile: [${root.name}]/${VcsFileUtil.relativePath(root, filePath)}"
+ }
+
+ internal inner class Refresh internal constructor(private val oldCachedData: CachedData?,
+ private val newCachedData: CachedData?,
+ oldModificationStamp: Long) {
+ val event: VFileContentChangeEvent = VFileContentChangeEvent(null, this@GitIndexVirtualFile, oldModificationStamp, -1,
+ 0, 0,
+ oldCachedData?.length ?: 0, newCachedData?.length ?: 0, true)
+
+ fun run(): Boolean {
+ LOG.debug("Refreshing ${this@GitIndexVirtualFile}")
+ return cachedData.compareAndSet(oldCachedData, newCachedData)
+ }
+
+ override fun toString(): String {
+ return "GitIndexVirtualFile.Refresh: ${this@GitIndexVirtualFile}"
+ }
+ }
+
+ companion object {
+ private val LOG = Logger.getInstance(GitIndexVirtualFile::class.java)
+ const val SEPARATOR = ':'
+
+ private fun encode(project: Project, root: VirtualFile, filePath: FilePath): String {
+ return StringUtil.escapeChar(project.locationHash, SEPARATOR) + SEPARATOR +
+ StringUtil.escapeChar(root.path, SEPARATOR) + SEPARATOR +
+ StringUtil.escapeChar(filePath.path, SEPARATOR)
+ }
+
+ fun decode(path: String): Triple? {
+ val components = path.split(SEPARATOR)
+ if (components.size != 3) return null
+
+ val locationHash = StringUtil.unescapeChar(components[0], SEPARATOR)
+ val project = ProjectManager.getInstance().openProjects.firstOrNull { it.locationHash == locationHash } ?: return null
+ val root = LocalFileSystem.getInstance().findFileByPath(StringUtil.unescapeChar(components[1], SEPARATOR)) ?: return null
+ val filePath = VcsUtil.getFilePath(StringUtil.unescapeChar(components[2], SEPARATOR))
+
+ return Triple(project, root, filePath)
+ }
+ }
+
+ internal data class CachedData(val hash: Hash, val length: Long, val isExecutable: Boolean = false)
+}
\ No newline at end of file
diff --git a/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFileCache.kt b/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFileCache.kt
new file mode 100644
index 000000000000..d1a18b9c5b93
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/index/vfs/GitIndexVirtualFileCache.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.index.vfs
+
+import com.google.common.cache.CacheBuilder
+import com.google.common.cache.CacheLoader
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vcs.FilePath
+import com.intellij.openapi.vfs.VirtualFile
+
+class GitIndexVirtualFileCache(private val project: Project) : Disposable {
+ private val cache = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from { key ->
+ GitIndexVirtualFile(project, key!!.root, key.filePath)
+ })
+
+ fun get(root: VirtualFile, filePath: FilePath): GitIndexVirtualFile {
+ return cache.get(Key(root, filePath))
+ }
+
+ fun forEachFile(function: (GitIndexVirtualFile) -> Unit) {
+ cache.asMap().forEach { (_, file) -> function(file) }
+ }
+
+ override fun dispose() {
+ cache.invalidateAll()
+ }
+
+ private data class Key(val root: VirtualFile, val filePath: FilePath)
+}
\ No newline at end of file
diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java
index 610dc1cc741a..055f96a1c007 100644
--- a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java
+++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java
@@ -361,6 +361,14 @@ public class GitRepositoryFiles {
VfsUtil.markDirtyAndRefresh(true, true, false, myMainDir, myWorktreeDir);
}
+ /**
+ * Refresh .git/index asynchronously.
+ */
+ public void refreshIndexFile() {
+ VirtualFile indexFilePath = LocalFileSystem.getInstance().refreshAndFindFileByPath(myIndexFilePath);
+ VfsUtil.markDirtyAndRefresh(true, false, false, indexFilePath);
+ }
+
/**
* Refresh that part of .git repository files, which is not covered by {@link GitRepository#update()}, e.g. the {@code refs/tags/} dir.
*
diff --git a/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.java b/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.java
index 801c3f642470..fde5f013ff26 100644
--- a/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.java
+++ b/plugins/git4idea/src/git4idea/repo/GitUntrackedFilesHolder.java
@@ -279,7 +279,7 @@ public class GitUntrackedFilesHolder implements Disposable, AsyncVfsEventsListen
return repository.getRepositoryFiles().isOrigHeadFile(path);
}
- private static boolean indexChanged(@NotNull GitRepository repository, @NotNull String path) {
+ public static boolean indexChanged(@NotNull GitRepository repository, @NotNull String path) {
return repository.getRepositoryFiles().isIndexFile(path);
}