[git-index] display unsaved changes in the tree

GitOrigin-RevId: e6e4868dde1c7c72bec3d625ff214e3a0d32f68f
This commit is contained in:
Julia Beliaeva
2020-05-09 05:25:10 +00:00
committed by intellij-monorepo-bot
parent 5cc00a7411
commit ffb627eff8
11 changed files with 236 additions and 74 deletions
@@ -69,4 +69,16 @@ fun ExecutorService.submitSafe(log: Logger, task: () -> Unit): Future<*> = this.
catch (t: Throwable) {
log.error(t)
}
}
fun <R> SingleTaskController<R, *>.sendRequests(vararg requests: R?): Boolean {
val notNullRequests = mutableListOf<R>()
for (request in requests) {
if (request != null) notNullRequests.add(request)
}
if (notNullRequests.isNotEmpty()) {
request(notNullRequests)
return true
}
return false
}
@@ -59,11 +59,15 @@ public abstract class SingleTaskController<Request, Result> implements Disposabl
* Otherwise just remembers requests in the queue. Later they can be retrieved by {@link #popRequests()}.
*/
public final void request(Request @NotNull ... requests) {
request(Arrays.asList(requests));
}
public void request(@NotNull List<Request> requestList) {
synchronized (LOCK) {
if (myIsClosed) return;
myAwaitingRequests.addAll(Arrays.asList(requests));
debug("Added requests: " + Arrays.toString(requests));
if (myRunningTask != null && cancelRunningTasks(requests)) {
myAwaitingRequests.addAll(requestList);
debug("Added requests: " + requestList);
if (myRunningTask != null && cancelRunningTasks(requestList)) {
cancelTask(myRunningTask);
}
if (myRunningTask == null) {
@@ -73,7 +77,7 @@ public abstract class SingleTaskController<Request, Result> implements Disposabl
}
}
protected boolean cancelRunningTasks(Request @NotNull [] requests) {
protected boolean cancelRunningTasks(@NotNull List<Request> requests) {
return false;
}
@@ -26,6 +26,7 @@ import com.intellij.vcs.log.data.SingleTaskController
import com.intellij.vcs.log.ui.details.commit.CommitDetailsPanel
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CalledInBackground
import org.jetbrains.annotations.NotNull
import java.awt.BorderLayout
import javax.swing.JPanel
import javax.swing.border.Border
@@ -144,5 +145,5 @@ private class ChangesLoadingController(
return SingleTaskImpl(future, indicator)
}
override fun cancelRunningTasks(requests: Array<out List<VcsCommitMetadata>>) = true
override fun cancelRunningTasks(requests: List<List<VcsCommitMetadata>>) = true
}
@@ -77,7 +77,7 @@ public class VisiblePackRefresherImpl implements VisiblePackRefresher, Disposabl
}
@Override
protected boolean cancelRunningTasks(Request @NotNull [] requests) {
protected boolean cancelRunningTasks(@NotNull List<Request> requests) {
return ContainerUtil.findInstance(requests, IndexingFinishedRequest.class) != null ||
ContainerUtil.findInstance(requests, FilterRequest.class) != null;
}
@@ -20,7 +20,7 @@ internal abstract class PredictionController(private val project: Project,
inProgressChanged(value)
}
override fun cancelRunningTasks(requests: Array<out PredictionRequest>) = true
override fun cancelRunningTasks(requests: List<PredictionRequest>) = true
override fun startNewBackgroundTask(): SingleTask {
val task: Task.Backgroundable = object : Task.Backgroundable(
@@ -146,11 +146,17 @@ abstract class GitFileStatusNodeProducerBase(val statusNode: GitFileStatusNode)
}
private fun GitFileStatusNode.has(contentVersion: ContentVersion): Boolean {
return status.has(contentVersion)
return when (this) {
is GitFileStatusNode.Saved -> status.has(contentVersion)
is GitFileStatusNode.Unsaved -> true
}
}
private fun GitFileStatusNode.path(contentVersion: ContentVersion): FilePath {
return status.path(contentVersion)
return when (this) {
is GitFileStatusNode.Saved -> status.path(contentVersion)
is GitFileStatusNode.Unsaved -> filePath
}
}
private fun getTitle(statusNode: GitFileStatusNode): String {
@@ -1,7 +1,14 @@
// 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
import com.intellij.AppTopics
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.util.ProgressIndicatorBase
import com.intellij.openapi.project.Project
@@ -18,7 +25,10 @@ import com.intellij.util.EventDispatcher
import com.intellij.util.messages.MessageBusConnection
import com.intellij.vcs.log.BaseSingleTaskController
import com.intellij.vcs.log.runInEdt
import com.intellij.vcs.log.sendRequests
import git4idea.GitVcs
import git4idea.index.vfs.GitIndexVirtualFile
import git4idea.index.vfs.filePath
import git4idea.repo.GitRepositoryManager
import git4idea.repo.GitUntrackedFilesHolder
import java.util.*
@@ -32,47 +42,77 @@ class GitStageTracker(val project: Project) : Disposable {
private set
@Volatile
var state: State = State(emptyMap())
var state: State = State.EMPTY
init {
val connection: MessageBusConnection = project.messageBus.connect(this)
connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: List<VFileEvent>) {
val roots = GitRepositoryManager.getInstance(project).repositories.filter { repo ->
events.any { e -> GitUntrackedFilesHolder.totalRefreshNeeded(repo, e.path) }
}.map { it.root }
scheduleUpdateRoots(roots)
scheduleUpdateForEvents(events)
}
})
connection.subscribe(VcsDirtyScopeManagerListener.VCS_DIRTY_SCOPE_UPDATED, object : VcsDirtyScopeManagerListener {
override fun everythingDirty() {
scheduleUpdateRoots(gitRoots())
scheduleUpdateAll()
}
override fun filePathsDirty(filesConverted: Map<VcsRoot, Set<FilePath>>, dirsConverted: Map<VcsRoot, Set<FilePath>>) {
val roots = filesConverted.keys.union(dirsConverted.keys).filter { it.vcs?.keyInstanceMethod == GitVcs.getKey() }.map { it.path }
scheduleUpdateRoots(roots)
singleTaskController.sendRequests(gitStateRequest(roots), unsavedFilesRequest(roots))
}
})
connection.subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED, VcsListener {
if (!scheduleUpdateRoots(gitRoots())) {
val roots = gitRoots()
if (!singleTaskController.sendRequests(gitStateRequest(roots), unsavedFilesRequest(roots))) {
runInEdt(this) {
update(State(emptyMap()))
update(State.EMPTY)
}
}
})
connection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, object : FileDocumentManagerListener {
override fun unsavedDocumentDropped(document: Document) {
scheduleUpdateUnsaved(document)
}
override fun unsavedDocumentsDropped() {
scheduleUpdateUnsaved()
}
})
EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
scheduleUpdateUnsaved(event.document)
}
}, this)
}
fun scheduleUpdateAll() {
scheduleUpdateRoots(gitRoots())
singleTaskController.sendRequests(gitStateRequest(gitRoots()), unsavedFilesRequest(gitRoots()))
}
private fun scheduleUpdateRoots(roots: List<VirtualFile>): Boolean {
if (roots.isNotEmpty()) {
singleTaskController.request(Request(roots))
return true
private fun scheduleUpdateUnsaved() {
singleTaskController.sendRequests(unsavedFilesRequest(gitRoots()))
}
private fun scheduleUpdateUnsaved(document: Document) {
val file = FileDocumentManager.getInstance().getFile(document) ?: return
getRoot(file)?.let { singleTaskController.request(Request.Unsaved(listOf(it))) }
}
private fun scheduleUpdateForEvents(events: List<VFileEvent>) {
val updateGitStateFor = GitRepositoryManager.getInstance(project).repositories.filter { repo ->
events.any { e -> GitUntrackedFilesHolder.totalRefreshNeeded(repo, e.path) }
}.map { it.root }
val updateAllFor = events.mapNotNull { it.file as? GitIndexVirtualFile }.mapTo(mutableSetOf()) { it.root }
singleTaskController.sendRequests(gitStateRequest(updateGitStateFor.union(updateAllFor)), unsavedFilesRequest(updateAllFor))
}
private fun getRoot(file: VirtualFile): VirtualFile? {
return when {
file is GitIndexVirtualFile -> file.root
file.isInLocalFileSystem -> ProjectLevelVcsManager.getInstance(project).getVcsRootFor(file)
else -> null
}
return false
}
fun addListener(listener: GitStageTrackerListener, disposable: Disposable) {
@@ -88,51 +128,104 @@ class GitStageTracker(val project: Project) : Disposable {
eventDispatcher.multicaster.update()
}
private fun gitStateRequest(roots: Collection<VirtualFile>): Request.GitState? {
if (roots.isEmpty()) return null
return Request.GitState(roots)
}
private fun unsavedFilesRequest(roots: Collection<VirtualFile>): Request.Unsaved? {
if (roots.isEmpty()) return null
return Request.Unsaved(roots)
}
override fun dispose() {
state = State(emptyMap())
state = State.EMPTY
}
private data class Request(val roots: Collection<VirtualFile>) {
override fun toString(): String = "Request(roots=${roots.joinToString(",") { it.name }})"
private sealed class Request(val roots: Collection<VirtualFile>) {
class GitState(roots: Collection<VirtualFile>) : Request(roots) {
override fun toString(): String = "Request.GitState(roots=${roots.joinToString(",") { it.name }})"
}
class Unsaved(roots: Collection<VirtualFile>) : Request(roots) {
override fun toString(): String = "Request.Unsaved(roots=${roots.joinToString(",") { it.name }})"
}
}
data class State(val gitState: Map<VirtualFile, List<GitFileStatus>>) {
data class State(val gitState: Map<VirtualFile, List<GitFileStatus>>,
val unsavedIndex: Map<VirtualFile, List<GitIndexVirtualFile>>,
val unsavedWorkTree: Map<VirtualFile, List<VirtualFile>>) {
private val roots: Collection<VirtualFile>
get() = gitState.keys
get() = gitState.keys.union(unsavedIndex.keys).union(unsavedWorkTree.keys)
val stagedRoots: Set<VirtualFile>
get() {
return gitState.filterValues {
it.any { line -> line.getStagedStatus() != null }
}.keys
}.keys.union(unsavedIndex.filterValues { it.isNotEmpty() }.keys)
}
fun hasStagedRoots(): Boolean {
return gitState.values.flatten().any { it.getStagedStatus() != null }
return gitState.values.flatten().any { it.getStagedStatus() != null } || unsavedIndex.values.flatten().isNotEmpty()
}
fun updatedWith(newState: State, roots: Collection<VirtualFile> = this.roots.union(newState.roots)): State {
val gitState = hashMapOf<VirtualFile, List<GitFileStatus>>()
val unsavedIndex = hashMapOf<VirtualFile, List<GitIndexVirtualFile>>()
val unsavedWorkTree = hashMapOf<VirtualFile, List<VirtualFile>>()
for (root in roots) {
gitState[root] = newState.gitState[root] ?: this.gitState[root] ?: emptyList()
unsavedIndex[root] = newState.unsavedIndex[root] ?: this.unsavedIndex[root] ?: emptyList()
unsavedWorkTree[root] = newState.unsavedWorkTree[root] ?: this.unsavedWorkTree[root] ?: emptyList()
}
return State(gitState)
return State(gitState, unsavedIndex, unsavedWorkTree)
}
override fun toString(): String {
return "State(gitState=$gitState)"
return "State(gitState=$gitState, unsavedIndex=$unsavedIndex, unsavedWorkTree=$unsavedWorkTree)"
}
companion object {
internal val EMPTY = State(emptyMap(), emptyMap(), emptyMap())
}
}
private inner class MySingleTaskController : BaseSingleTaskController<Request, State>("Git Stage Tracker",
this::update, this) {
override fun process(requests: List<Request>, previousState: State?): State {
val gitStateRoots = requests.flatMapTo(HashSet()) { it.roots }
val gitStateRoots = requests.filterIsInstance<Request.GitState>().flatMapTo(HashSet()) { it.roots }
val gitState = mutableMapOf<VirtualFile, List<GitFileStatus>>()
for (root in gitStateRoots) {
gitState[root] = getStatus(project, root, withIgnored = false).map { GitFileStatus(root, it) }
}
val newState = State(gitState)
val unsavedRoots = requests.filterIsInstance<Request.Unsaved>().flatMapTo(HashSet()) { it.roots }
val index = unsavedRoots.associateWith { mutableListOf<GitIndexVirtualFile>() }
val workTree = unsavedRoots.associateWith { mutableListOf<VirtualFile>() }
for (document in FileDocumentManager.getInstance().unsavedDocuments) {
val file = FileDocumentManager.getInstance().getFile(document) ?: continue
if (!file.isValid || !FileDocumentManager.getInstance().isFileModified(file)) continue
val root = getRoot(file) ?: continue
if (!unsavedRoots.contains(root)) continue
val statusRecord: GitFileStatus? = if (gitStateRoots.contains(root)) {
gitState[root]?.find { it.path == file.filePath() }
}
else if (previousState?.gitState?.containsKey(root) == true) {
previousState.gitState[root]?.find { it.path == file.filePath() }
}
else {
state.gitState[root]?.find { it.path == file.filePath() }
}
if (statusRecord?.isTracked() == false) continue
if (file is GitIndexVirtualFile && statusRecord?.getStagedStatus() == null) {
index.getValue(root).add(file)
}
else if (file.isInLocalFileSystem && statusRecord?.getUnStagedStatus() == null) {
workTree.getValue(root).add(file)
}
}
val newState = State(gitState, index, workTree)
return previousState?.updatedWith(newState) ?: newState
}
@@ -12,9 +12,9 @@ import com.intellij.openapi.vcs.changes.ui.*
import com.intellij.openapi.vcs.impl.PlatformVcsPathPresenter
import com.intellij.openapi.vfs.VirtualFile
import git4idea.i18n.GitBundle
import git4idea.index.GitFileStatus
import git4idea.index.GitStageTracker
import git4idea.index.isRenamed
import git4idea.index.*
import git4idea.index.vfs.GitIndexVirtualFile
import git4idea.index.vfs.filePath
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
@@ -38,12 +38,26 @@ abstract class GitStageTree(project: Project) : ChangesTree(project, false, true
NodeKind.values().forEach { kind ->
if (kind.`is`(status)) {
val parentNode = parentNodes.getOrPut(kind) { ChangesBrowserKindNode(kind) }
val fileStatusInfo = GitFileStatusNode(root, status, kind)
val fileStatusInfo = GitFileStatusNode.Saved(root, status, kind)
builder.insertPath(fileStatusInfo, parentNode)
}
}
}
}
state.unsavedIndex.forEach { (root, files) ->
files.forEach { file ->
val parentNode = parentNodes.getOrPut(NodeKind.STAGED) { ChangesBrowserKindNode(NodeKind.STAGED) }
val fileStatusInfo = GitFileStatusNode.Unsaved(root, file)
builder.insertPath(fileStatusInfo, parentNode)
}
}
state.unsavedWorkTree.forEach { (root, files) ->
files.forEach { file ->
val parentNode = parentNodes.getOrPut(NodeKind.UNSTAGED) { ChangesBrowserKindNode(NodeKind.UNSTAGED) }
val fileStatusInfo = GitFileStatusNode.Unsaved(root, file)
builder.insertPath(fileStatusInfo, parentNode)
}
}
parentNodes.values.forEach { builder.insertIntoRootNode(it) }
@@ -130,29 +144,63 @@ enum class NodeKind(@PropertyKey(resourceBundle = GitBundle.BUNDLE) @NonNls val
open fun origPath(status: GitFileStatus): FilePath? = null
}
class GitFileStatusNode(val root: VirtualFile, val status: GitFileStatus, val kind: NodeKind) {
val filePath: FilePath get() = status.path
val origPath: FilePath? get() = kind.origPath(status)
val fileStatus: FileStatus get() = kind.status(status)
sealed class GitFileStatusNode(val root: VirtualFile, val kind: NodeKind) {
abstract val filePath: FilePath
abstract val origPath: FilePath?
abstract val fileStatus: FileStatus
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
class Saved(root: VirtualFile, val status: GitFileStatus, kind: NodeKind) : GitFileStatusNode(root, kind) {
override val filePath: FilePath get() = status.path
override val origPath: FilePath? get() = kind.origPath(status)
override val fileStatus: FileStatus get() = kind.status(status)
other as GitFileStatusNode
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (root != other.root) return false
if (status != other.status) return false
if (kind != other.kind) return false
other as Saved
return true
if (root != other.root) return false
if (status != other.status) return false
if (kind != other.kind) return false
return true
}
override fun hashCode(): Int {
return Objects.hashCode(root, fileStatus, kind)
}
override fun toString(): String {
return "GitFileStatusNode.Saved(root=$root, status=$fileStatus, kind=$kind)"
}
}
override fun hashCode(): Int {
return Objects.hashCode(root, fileStatus, kind)
}
class Unsaved(root: VirtualFile, val file: VirtualFile) : GitFileStatusNode(root, file.kind()) {
override val filePath: FilePath get() = file.filePath()
override val origPath: FilePath? get() = null
override val fileStatus: FileStatus get() = FileStatus.MODIFIED
override fun toString(): String {
return "GitFileStatusNode.Saved(root=$root, status=$fileStatus, kind=$kind)"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Unsaved
if (root != other.root) return false
if (file != other.file) return false
return true
}
override fun hashCode(): Int {
return Objects.hashCode(root, file)
}
override fun toString(): String {
return "GitFileStatusNode.Unsaved(root=$root, file=$file"
}
}
}
}
private fun VirtualFile.kind() = if (this is GitIndexVirtualFile) NodeKind.STAGED else NodeKind.UNSTAGED
@@ -203,4 +203,9 @@ class GitIndexVirtualFile(private val project: Project,
}
internal data class CachedData(val hash: Hash, val length: Long, val isExecutable: Boolean = false)
}
internal fun VirtualFile.filePath(): FilePath {
return if (this is GitIndexVirtualFile) this.filePath
else VcsUtil.getFilePath(this)
}
@@ -19,6 +19,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.vcs.log.BaseSingleTaskController
import git4idea.index.isTracked
import git4idea.index.repositoryPath
import org.jetbrains.annotations.NotNull
private val LOG = Logger.getInstance("#git4idea.light.LightGitEditorHighlighterManager")
@@ -114,7 +115,7 @@ class LightGitEditorHighlighterManager(val tracker: LightGitTracker) : Disposabl
}
}
override fun cancelRunningTasks(requests: Array<out Request>?): Boolean = true
override fun cancelRunningTasks(requests: List<Request>): Boolean = true
}
private data class Request(val file: VirtualFile, val repositoryPath: String)
@@ -17,6 +17,7 @@ import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.EventDispatcher
import com.intellij.vcs.log.BaseSingleTaskController
import com.intellij.vcs.log.runInEdt
import com.intellij.vcs.log.sendRequests
import com.intellij.vcsUtil.VcsUtil
import git4idea.config.GitExecutableManager
import git4idea.config.GitVersionIdentificationException
@@ -59,8 +60,8 @@ class LightGitTracker : Disposable {
singleTaskController.request(Request.CheckGit)
runInEdt(this) {
sendRequests(locationRequest(lightEditService.selectedFile),
statusRequest(lightEditorManager.openFiles))
singleTaskController.sendRequests(locationRequest(lightEditService.selectedFile),
statusRequest(lightEditorManager.openFiles))
}
}
@@ -124,15 +125,6 @@ class LightGitTracker : Disposable {
return Request.Status(filesForRequest)
}
private fun sendRequests(vararg requests: Request?): Boolean {
val notNullRequests = requests.filterNotNullTo(mutableListOf())
if (notNullRequests.isNotEmpty()) {
singleTaskController.request(*notNullRequests.toTypedArray())
return true
}
return false
}
fun addUpdateListener(listener: LightGitTrackerListener, parent: Disposable) {
eventDispatcher.addListener(listener, parent)
}
@@ -145,15 +137,15 @@ class LightGitTracker : Disposable {
if (!hasGit) return
val targetFiles = events.filter { it.isFromSave || it.isFromRefresh }.mapNotNullTo(mutableSetOf()) { it.file }
sendRequests(statusRequest(lightEditorManager.openFiles.intersect(targetFiles)))
singleTaskController.sendRequests(statusRequest(lightEditorManager.openFiles.intersect(targetFiles)))
}
}
private inner class MyFrameStateListener : FrameStateListener {
override fun onFrameActivated() {
sendRequests(Request.CheckGit,
locationRequest(lightEditService.selectedFile),
statusRequest(lightEditorManager.openFiles))
singleTaskController.sendRequests(Request.CheckGit,
locationRequest(lightEditService.selectedFile),
statusRequest(lightEditorManager.openFiles))
}
}
@@ -164,7 +156,7 @@ class LightGitTracker : Disposable {
state = state.copy(location = null)
val selectedFile = editorInfo?.file
if (!sendRequests(locationRequest(selectedFile), statusRequest(listOf(selectedFile)))) {
if (!singleTaskController.sendRequests(locationRequest(selectedFile), statusRequest(listOf(selectedFile)))) {
runInEdt(this@LightGitTracker) { eventDispatcher.multicaster.update() }
}
}