[git-index] add diff preview

GitOrigin-RevId: 0544268488f3f3aa5539ec7aed09fb7bf7ed372d
This commit is contained in:
Julia Beliaeva
2020-05-09 05:25:10 +00:00
committed by intellij-monorepo-bot
parent f01442dfe6
commit 5cc00a7411
8 changed files with 297 additions and 15 deletions
@@ -48,15 +48,18 @@ abstract class BaseSingleTaskController<Request, Result>(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()
@@ -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...
@@ -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)
}
}
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 }
@@ -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') ||
@@ -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<GitIndexVirtualFileCache>().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<DiffContent>, titles: List<String>, 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)
}
@@ -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<Wrapper?> {
val hasSelection = tree.selectionModel.selectionCount != 0
return wrap(if (hasSelection) VcsTreeModelData.selected(tree) else VcsTreeModelData.all(tree))
}
override fun getAllChanges(): Stream<Wrapper?> {
return wrap(VcsTreeModelData.all(tree))
}
private fun wrap(modelData: VcsTreeModelData): Stream<Wrapper?> {
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)
}
}
}
@@ -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) {
@@ -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<GitFileStatusNode>(node, node.status) {
AbstractChangesBrowserFilePathNode<GitFileStatusNode>(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)"
}
}