From 5cc00a74112ad618c2412ec65437c65db3d9789c Mon Sep 17 00:00:00 2001 From: Julia Beliaeva Date: Wed, 19 Feb 2020 17:51:37 +0300 Subject: [PATCH] [git-index] add diff preview GitOrigin-RevId: 0544268488f3f3aa5539ec7aed09fb7bf7ed372d --- .../vcs/log/BaseSingleTaskController.kt | 9 +- .../resources/messages/GitBundle.properties | 2 + .../src/git4idea/index/GitFileStatus.kt | 21 ++- .../src/git4idea/index/GitIndexStatusUtil.kt | 2 + .../src/git4idea/index/GitStageDiffUtil.kt | 158 ++++++++++++++++++ .../git4idea/index/ui/GitStageDiffPreview.kt | 66 ++++++++ .../src/git4idea/index/ui/GitStagePanel.kt | 14 +- .../src/git4idea/index/ui/GitStageTree.kt | 40 ++++- 8 files changed, 297 insertions(+), 15 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/index/GitStageDiffUtil.kt create mode 100644 plugins/git4idea/src/git4idea/index/ui/GitStageDiffPreview.kt diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/BaseSingleTaskController.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/BaseSingleTaskController.kt index a63f4458e998..be1d51beeef1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/BaseSingleTaskController.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/BaseSingleTaskController.kt @@ -48,15 +48,18 @@ abstract class BaseSingleTaskController(name: String, resultCon } fun runInEdt(disposable: Disposable, action: () -> Unit) { - val app = ApplicationManager.getApplication() - if (app.isDispatchThread) { + if (ApplicationManager.getApplication().isDispatchThread) { action() } else { - app.invokeLater(action, { Disposer.isDisposed(disposable) }) + runInEdtAsync(disposable, action) } } +fun runInEdtAsync(disposable: Disposable, action: () -> Unit) { + ApplicationManager.getApplication().invokeLater(action, { Disposer.isDisposed(disposable) }) +} + fun ExecutorService.submitSafe(log: Logger, task: () -> Unit): Future<*> = this.submit { try { task() diff --git a/plugins/git4idea/resources/messages/GitBundle.properties b/plugins/git4idea/resources/messages/GitBundle.properties index 7accdc5d2cea..b7324c5a4649 100644 --- a/plugins/git4idea/resources/messages/GitBundle.properties +++ b/plugins/git4idea/resources/messages/GitBundle.properties @@ -563,6 +563,8 @@ stage.tree.node.unstaged=Not staged for commit stage.tree.node.unmerged=Unmerged stage.tree.node.untracked=Untracked stage.tree.node.ignored=Ignored +stage.content.staged=Staged +stage.content.local=Local stage.commit.process=Committing Staged Changes... stage.loading.status=Loading changes... diff --git a/plugins/git4idea/src/git4idea/index/GitFileStatus.kt b/plugins/git4idea/src/git4idea/index/GitFileStatus.kt index 49137a9aa47e..61560d2b1c3f 100644 --- a/plugins/git4idea/src/git4idea/index/GitFileStatus.kt +++ b/plugins/git4idea/src/git4idea/index/GitFileStatus.kt @@ -18,8 +18,27 @@ data class GitFileStatus(val index: StatusCode, fun isUntracked() = isUntracked(index) || isUntracked(workTree) fun isIgnored() = isIgnored(index) || isIgnored(workTree) + fun isTracked() = !isIgnored(index) && !isUntracked(index) fun getStagedStatus(): FileStatus? = if (isIgnored(index) || isUntracked(index)) null else getFileStatus(index) fun getUnStagedStatus(): FileStatus? = if (isIgnored(workTree) || isUntracked(workTree)) null else getFileStatus(workTree) -} \ No newline at end of file +} + +fun GitFileStatus.has(contentVersion: ContentVersion): Boolean { + return when (contentVersion) { + ContentVersion.HEAD -> isTracked() && !isAdded(index) + ContentVersion.STAGED -> isTracked() && !isDeleted(index) + ContentVersion.LOCAL -> !isDeleted(workTree) + } +} + +fun GitFileStatus.path(contentVersion: ContentVersion): FilePath { + return when (contentVersion) { + ContentVersion.HEAD -> origPath ?: path + ContentVersion.STAGED -> if (isRenamed(index)) path else origPath ?: path + ContentVersion.LOCAL -> path + } +} + +enum class ContentVersion { HEAD, STAGED, LOCAL } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/index/GitIndexStatusUtil.kt b/plugins/git4idea/src/git4idea/index/GitIndexStatusUtil.kt index 7ec327b53405..ea6e2fa232ce 100644 --- a/plugins/git4idea/src/git4idea/index/GitIndexStatusUtil.kt +++ b/plugins/git4idea/src/git4idea/index/GitIndexStatusUtil.kt @@ -168,6 +168,8 @@ typealias StatusCode = Char internal fun isIgnored(status: StatusCode) = status == '!' internal fun isUntracked(status: StatusCode) = status == '?' fun isRenamed(status: StatusCode) = status == 'R' || status == 'C' +internal fun isAdded(status: StatusCode) = status == 'A' +internal fun isDeleted(status: StatusCode) = status == 'D' internal fun isConflicted(index: StatusCode, workTree: StatusCode): Boolean { return (index == 'D' && workTree == 'D') || (index == 'A' && workTree == 'A') || diff --git a/plugins/git4idea/src/git4idea/index/GitStageDiffUtil.kt b/plugins/git4idea/src/git4idea/index/GitStageDiffUtil.kt new file mode 100644 index 000000000000..0e0a4c56d98f --- /dev/null +++ b/plugins/git4idea/src/git4idea/index/GitStageDiffUtil.kt @@ -0,0 +1,158 @@ +// 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.diff.DiffContentFactory +import com.intellij.diff.DiffContentFactoryEx +import com.intellij.diff.DiffRequestFactoryImpl +import com.intellij.diff.chains.DiffRequestProducerException +import com.intellij.diff.contents.DiffContent +import com.intellij.diff.contents.DocumentContent +import com.intellij.diff.requests.DiffRequest +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.FileStatus +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.changes.actions.diff.UnversionedDiffRequestProducer +import com.intellij.openapi.vcs.changes.ui.ChangeDiffRequestChain +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.vcsUtil.VcsFileUtil +import git4idea.GitUtil +import git4idea.i18n.GitBundle +import git4idea.index.ui.GitFileStatusNode +import git4idea.index.ui.NodeKind +import git4idea.index.vfs.GitIndexVirtualFile +import git4idea.index.vfs.GitIndexVirtualFileCache +import git4idea.util.GitFileUtils +import java.io.IOException + +fun createTwoSidesDiffRequestProducer(project: Project, statusNode: GitFileStatusNode): ChangeDiffRequestChain.Producer { + return when (statusNode.kind) { + NodeKind.STAGED -> StagedProducer(project, statusNode) + NodeKind.UNSTAGED -> UnStagedProducer(project, statusNode) + NodeKind.CONFLICTED, NodeKind.IGNORED, NodeKind.UNTRACKED -> UnversionedDiffRequestProducer.create(project, statusNode.filePath) + } +} + +@Throws(VcsException::class, IOException::class) +private fun headContent(project: Project, statusNode: GitFileStatusNode): DiffContent { + if (!statusNode.has(ContentVersion.HEAD)) return DiffContentFactory.getInstance().createEmpty() + + val headContent = GitFileUtils.getFileContent(project, statusNode.root, GitUtil.HEAD, + VcsFileUtil.relativePath(statusNode.root, statusNode.path(ContentVersion.HEAD))) + return DiffContentFactoryEx.getInstanceEx().createFromBytes(project, headContent, statusNode.filePath) +} + +@Throws(VcsException::class) +private fun stagedContent(project: Project, statusNode: GitFileStatusNode): DiffContent { + if (!statusNode.has(ContentVersion.STAGED)) return DiffContentFactory.getInstance().createEmpty() + + val indexFile = project.service().get(statusNode.root, statusNode.path(ContentVersion.STAGED)) + return DiffContentFactory.getInstance().create(project, indexFile) +} + +@Throws(VcsException::class) +private fun localContent(project: Project, statusNode: GitFileStatusNode): DiffContent { + if (!statusNode.has(ContentVersion.LOCAL)) return DiffContentFactory.getInstance().createEmpty() + + val localFile: VirtualFile = statusNode.path(ContentVersion.LOCAL).virtualFile ?: + throw VcsException("Can't get local file: " + statusNode.filePath) + return DiffContentFactory.getInstance().create(project, localFile) +} + +private class UnStagedProducer constructor(private val project: Project, file: GitFileStatusNode) : GitFileStatusNodeProducerBase(file) { + @Throws(VcsException::class) + override fun processImpl(): DiffRequest { + return StagedDiffRequest(stagedContent(project, statusNode), localContent(project, statusNode), + GitBundle.message("stage.content.staged"), GitBundle.message("stage.content.local"), + getTitle(statusNode)) + } +} + +private class StagedProducer constructor(private val project: Project, file: GitFileStatusNode) : GitFileStatusNodeProducerBase(file) { + @Throws(VcsException::class, IOException::class) + override fun processImpl(): DiffRequest { + return StagedDiffRequest(headContent(project, statusNode), stagedContent(project, statusNode), + GitUtil.HEAD, GitBundle.message("stage.content.staged"), + getTitle(statusNode)) + } +} + +private class StagedDiffRequest(contents: List, titles: List, title: String? = null) : + SimpleDiffRequest(title, contents, titles) { + + constructor(content1: DiffContent, content2: DiffContent, title1: String, title2: String, title: String? = null) : + this(listOf(content1, content2), listOf(title1, title2), title) + + override fun onAssigned(isAssigned: Boolean) { + super.onAssigned(isAssigned) + if (!isAssigned) { + for (content in contents) { + if (content is DocumentContent) { + val file = FileDocumentManager.getInstance().getFile(content.document) + if (file is GitIndexVirtualFile) { + FileDocumentManager.getInstance().saveDocument(content.document) + } + } + } + } + } +} + +abstract class GitFileStatusNodeProducerBase(val statusNode: GitFileStatusNode) : ChangeDiffRequestChain.Producer { + @Throws(VcsException::class, IOException::class) + abstract fun processImpl(): DiffRequest + + @Throws(DiffRequestProducerException::class) + override fun process(context: UserDataHolder, indicator: ProgressIndicator): DiffRequest { + try { + return processImpl() + } + catch (e: VcsException) { + throw DiffRequestProducerException(e) + } + catch (e: IOException) { + throw DiffRequestProducerException(e) + } + } + + override fun getFilePath(): FilePath { + return statusNode.filePath + } + + override fun getFileStatus(): FileStatus { + return statusNode.fileStatus + } + + override fun getName(): String { + return statusNode.filePath.presentableUrl + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + + val wrapper = o as GitFileStatusNodeProducerBase + return statusNode == wrapper.statusNode + } + + override fun hashCode(): Int { + return statusNode.hashCode() + } +} + +private fun GitFileStatusNode.has(contentVersion: ContentVersion): Boolean { + return status.has(contentVersion) +} + +private fun GitFileStatusNode.path(contentVersion: ContentVersion): FilePath { + return status.path(contentVersion) +} + +private fun getTitle(statusNode: GitFileStatusNode): String { + return DiffRequestFactoryImpl.getTitle(statusNode.filePath, statusNode.origPath, DiffRequestFactoryImpl.DIFF_TITLE_RENAME_SEPARATOR) +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/index/ui/GitStageDiffPreview.kt b/plugins/git4idea/src/git4idea/index/ui/GitStageDiffPreview.kt new file mode 100644 index 000000000000..12e7f1c67f60 --- /dev/null +++ b/plugins/git4idea/src/git4idea/index/ui/GitStageDiffPreview.kt @@ -0,0 +1,66 @@ +// 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.ui + +import com.intellij.diff.FrameDiffTool +import com.intellij.diff.chains.DiffRequestProducer +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vcs.changes.ChangeViewDiffRequestProcessor +import com.intellij.openapi.vcs.changes.ui.ChangesTree +import com.intellij.openapi.vcs.changes.ui.VcsTreeModelData +import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.SideBorder +import com.intellij.vcs.log.runInEdtAsync +import git4idea.index.GitStageTracker +import git4idea.index.GitStageTrackerListener +import git4idea.index.createTwoSidesDiffRequestProducer +import java.util.stream.Stream + +class GitStageDiffPreview(project: Project, private val tree: ChangesTree, tracker: GitStageTracker, parent: Disposable) : + ChangeViewDiffRequestProcessor(project, "Stage") { + + init { + myContentPanel.border = IdeBorderFactory.createBorder(SideBorder.TOP) + tree.addSelectionListener(Runnable { + val modelUpdateInProgress = tree.isModelUpdateInProgress + runInEdtAsync(this) { updatePreview(component.isShowing, modelUpdateInProgress) } + }, this) + tracker.addListener(object : GitStageTrackerListener { + override fun update() { + updatePreview(component.isShowing, true) + } + }, this) + Disposer.register(parent, this) + } + + override fun shouldAddToolbarBottomBorder(toolbarComponents: FrameDiffTool.ToolbarComponents): Boolean = false + + fun getToolbarWrapper(): com.intellij.ui.components.panels.Wrapper = myToolbarWrapper + + override fun selectChange(change: Wrapper) { + } + + override fun getSelectedChanges(): Stream { + val hasSelection = tree.selectionModel.selectionCount != 0 + return wrap(if (hasSelection) VcsTreeModelData.selected(tree) else VcsTreeModelData.all(tree)) + } + + override fun getAllChanges(): Stream { + return wrap(VcsTreeModelData.all(tree)) + } + + private fun wrap(modelData: VcsTreeModelData): Stream { + return modelData.userObjectsStream(GitFileStatusNode::class.java).map { info -> GitFileStatusNodeWrapper(info) } + } + + private class GitFileStatusNodeWrapper(val node: GitFileStatusNode) : Wrapper() { + override fun getPresentableName(): String? = node.filePath.name + + override fun getUserObject(): Any = node + + override fun createProducer(project: Project?): DiffRequestProducer? { + return createTwoSidesDiffRequestProducer(project!!, node) + } + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt b/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt index 07f0999dcbd1..bd6911663789 100644 --- a/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt +++ b/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt @@ -52,8 +52,18 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar treeMessageSplitter.firstComponent = progressStripe treeMessageSplitter.secondComponent = commitPanel - add(toolbar.component, BorderLayout.NORTH) - add(treeMessageSplitter, BorderLayout.CENTER) + val leftPanel = JPanel(BorderLayout()) + leftPanel.add(toolbar.component, BorderLayout.NORTH) + leftPanel.add(treeMessageSplitter, BorderLayout.CENTER) + + val diffPreview = GitStageDiffPreview(project, tree, tracker, this) + diffPreview.getToolbarWrapper().setVerticalSizeReferent(toolbar.component) + + val commitDiffSplitter = OnePixelSplitter("git.stage.commit.diff.splitter", 0.5f) + commitDiffSplitter.firstComponent = leftPanel + commitDiffSplitter.secondComponent = diffPreview.component + + add(commitDiffSplitter, BorderLayout.CENTER) tracker.addListener(MyGitStageTrackerListener(), this) if (tracker.isRefreshInProgress) { diff --git a/plugins/git4idea/src/git4idea/index/ui/GitStageTree.kt b/plugins/git4idea/src/git4idea/index/ui/GitStageTree.kt index 1b6b0d7ca849..33ee03e8ba47 100644 --- a/plugins/git4idea/src/git4idea/index/ui/GitStageTree.kt +++ b/plugins/git4idea/src/git4idea/index/ui/GitStageTree.kt @@ -1,6 +1,7 @@ // 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.ui +import com.google.common.base.Objects import com.intellij.ide.util.treeView.TreeState import com.intellij.openapi.project.Project import com.intellij.openapi.util.Comparing @@ -37,7 +38,7 @@ 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.path, kind, kind.status(status), kind.origPath(status)) + val fileStatusInfo = GitFileStatusNode(root, status, kind) builder.insertPath(fileStatusInfo, parentNode) } } @@ -63,7 +64,7 @@ abstract class GitStageTree(project: Project) : ChangesTree(project, false, true } private class ChangesBrowserGitFileStatusNode(node: GitFileStatusNode) : - AbstractChangesBrowserFilePathNode(node, node.status) { + AbstractChangesBrowserFilePathNode(node, node.fileStatus) { private val movedRelativePath by lazy { getMovedRelativePath(getUserObject()) } override fun filePath(userObject: GitFileStatusNode): FilePath = userObject.filePath override fun originText(userObject: GitFileStatusNode): String? { @@ -75,8 +76,8 @@ abstract class GitStageTree(project: Project) : ChangesTree(project, false, true } private fun getMovedRelativePath(userObject: GitFileStatusNode): String? { - if (userObject.origPath == null || userObject.origPath.parentPath == userObject.filePath.parentPath) return null - return PlatformVcsPathPresenter.getPresentableRelativePath(userObject.filePath, userObject.origPath) + if (userObject.origPath == null || userObject.origPath!!.parentPath == userObject.filePath.parentPath) return null + return PlatformVcsPathPresenter.getPresentableRelativePath(userObject.filePath, userObject.origPath!!) } } @@ -129,8 +130,29 @@ enum class NodeKind(@PropertyKey(resourceBundle = GitBundle.BUNDLE) @NonNls val open fun origPath(status: GitFileStatus): FilePath? = null } -data class GitFileStatusNode(val root: VirtualFile, - val filePath: FilePath, - val kind: NodeKind, - val status: FileStatus, - val origPath: 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) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GitFileStatusNode + + 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)" + } +} \ No newline at end of file