1 // 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.
2 package git4idea.index.ui
4 import com.intellij.ide.DataManager
5 import com.intellij.openapi.Disposable
6 import com.intellij.openapi.actionSystem.*
7 import com.intellij.openapi.fileEditor.FileDocumentManager
8 import com.intellij.openapi.progress.util.ProgressWindow
9 import com.intellij.openapi.project.Project
10 import com.intellij.openapi.util.Disposer
11 import com.intellij.openapi.vcs.AbstractVcsHelper
12 import com.intellij.openapi.vcs.VcsException
13 import com.intellij.openapi.vcs.VcsNotifier
14 import com.intellij.openapi.vcs.VcsRoot
15 import com.intellij.openapi.vcs.changes.ui.ChangesTree
16 import com.intellij.openapi.vcs.changes.ui.TreeActionsToolbarPanel
17 import com.intellij.openapi.vfs.VirtualFile
18 import com.intellij.openapi.wm.IdeFocusManager
19 import com.intellij.ui.OnePixelSplitter
20 import com.intellij.ui.PopupHandler
21 import com.intellij.ui.ScrollPaneFactory
22 import com.intellij.ui.SideBorder
23 import com.intellij.util.EditSourceOnDoubleClickHandler
24 import com.intellij.util.OpenSourceUtil
25 import com.intellij.util.Processor
26 import com.intellij.util.ui.UIUtil
27 import com.intellij.vcs.commit.getDefaultCommitShortcut
28 import com.intellij.vcs.commit.showEmptyCommitMessageConfirmation
29 import com.intellij.vcs.log.runInEdt
30 import com.intellij.vcs.log.runInEdtAsync
31 import com.intellij.vcs.log.ui.frame.ProgressStripe
32 import com.intellij.vcsUtil.VcsImplUtil
33 import com.intellij.xml.util.XmlStringUtil
34 import git4idea.GitVcs
35 import git4idea.conflicts.GitMergeHandler
36 import git4idea.conflicts.getConflictOperationLock
37 import git4idea.conflicts.showMergeWindow
38 import git4idea.i18n.GitBundle
39 import git4idea.index.CommitListener
40 import git4idea.index.GitStageTracker
41 import git4idea.index.GitStageTrackerListener
42 import git4idea.index.actions.*
43 import git4idea.merge.GitDefaultMergeDialogCustomizer
44 import git4idea.merge.GitMergeUtil
45 import git4idea.repo.GitConflict
46 import git4idea.repo.GitRepository
47 import git4idea.repo.GitRepositoryManager
48 import git4idea.status.GitChangeProvider
49 import one.util.streamex.StreamEx
50 import org.jetbrains.annotations.CalledInAwt
51 import java.awt.BorderLayout
52 import javax.swing.JPanel
54 val GIT_STAGE_TRACKER = DataKey.create<GitStageTracker>("GitStageTracker")
56 internal class GitStagePanel(private val tracker: GitStageTracker, disposableParent: Disposable) :
57 JPanel(BorderLayout()), DataProvider, Disposable {
58 private val project = tracker.project
60 private val tree: GitStageTree
61 private val commitPanel: GitCommitPanel
62 private val progressStripe: ProgressStripe
64 private val state: GitStageTracker.State
67 private var isCommitInProgress = false
68 private var hasPendingUpdates = false
71 tree = MyChangesTree(project)
73 commitPanel = MyGitCommitPanel()
74 commitPanel.createCommitAction().registerCustomShortcutSet(getDefaultCommitShortcut(), this)
76 val toolbarGroup = DefaultActionGroup()
77 toolbarGroup.add(ActionManager.getInstance().getAction("Git.Stage.Toolbar"))
78 toolbarGroup.addSeparator()
79 toolbarGroup.add(ActionManager.getInstance().getAction(ChangesTree.GROUP_BY_ACTION_GROUP))
80 toolbarGroup.addSeparator()
81 toolbarGroup.addAll(TreeActionsToolbarPanel.createTreeActions(tree))
82 val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarGroup, true)
83 toolbar.setTargetComponent(tree)
85 PopupHandler.installPopupHandler(tree, "Git.Stage.Tree.Menu", "Git.Stage.Tree.Menu")
87 val scrolledTree = ScrollPaneFactory.createScrollPane(tree, SideBorder.TOP)
88 progressStripe = ProgressStripe(scrolledTree, this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
89 val treeMessageSplitter = OnePixelSplitter(true, "git.stage.tree.message.splitter", 0.7f)
90 treeMessageSplitter.firstComponent = progressStripe
91 treeMessageSplitter.secondComponent = commitPanel
93 val leftPanel = JPanel(BorderLayout())
94 leftPanel.add(toolbar.component, BorderLayout.NORTH)
95 leftPanel.add(treeMessageSplitter, BorderLayout.CENTER)
97 val diffPreview = GitStageDiffPreview(project, tree, tracker, this)
98 diffPreview.getToolbarWrapper().setVerticalSizeReferent(toolbar.component)
100 val commitDiffSplitter = OnePixelSplitter("git.stage.commit.diff.splitter", 0.5f)
101 commitDiffSplitter.firstComponent = leftPanel
102 commitDiffSplitter.secondComponent = diffPreview.component
104 add(commitDiffSplitter, BorderLayout.CENTER)
106 tracker.addListener(MyGitStageTrackerListener(), this)
107 project.messageBus.connect(this).subscribe(GitChangeProvider.TOPIC, MyGitChangeProviderListener())
108 if (GitVcs.getInstance(project).changeProvider?.isRefreshInProgress == true) {
109 tree.setEmptyText(GitBundle.message("stage.loading.status"))
110 progressStripe.startLoadingImmediately()
113 Disposer.register(disposableParent, this)
115 runInEdtAsync(this, { tree.rebuildTree() })
118 private fun performCommit(amend: Boolean) {
119 val rootsToCommit = state.stagedRoots
120 if (rootsToCommit.isEmpty()) return
122 val commitMessage = commitPanel.commitMessage.text
123 if (commitMessage.isBlank() && !showEmptyCommitMessageConfirmation()) return
127 FileDocumentManager.getInstance().saveAllDocuments()
128 git4idea.index.performCommit(project, rootsToCommit, commitMessage, amend, MyCommitListener(commitMessage))
132 private fun commitStarted() {
133 isCommitInProgress = true
134 commitPanel.commitButton.isEnabled = false
138 private fun commitFinished(success: Boolean) {
139 isCommitInProgress = false
140 // commit button is going to be enabled after state update
141 if (success) commitPanel.isAmend = false
142 if (hasPendingUpdates) {
143 hasPendingUpdates = false
150 if (isCommitInProgress) {
151 hasPendingUpdates = true
155 commitPanel.commitButton.isEnabled = state.hasStagedRoots()
158 override fun getData(dataId: String): Any? {
159 if (GIT_STAGE_TRACKER.`is`(dataId)) return tracker
163 override fun dispose() {
166 private inner class MyChangesTree(project: Project) : GitStageTree(project, this) {
168 get() = this@GitStagePanel.state
169 override val operations: List<StagingAreaOperation> = listOf(GitAddOperation, GitResetOperation)
172 doubleClickHandler = Processor { e ->
173 if (EditSourceOnDoubleClickHandler.isToggleEvent(this, e)) return@Processor false
175 val mergeHandler = createMergeHandler(myProject)
176 val conflicts = getConflictsToMerge(mergeHandler)
177 if (conflicts.isEmpty()) {
178 OpenSourceUtil.openSourcesFrom(DataManager.getInstance().getDataContext(this), true)
181 showMergeWindow(project, mergeHandler, conflicts, myProject::isReversedRoot)
187 override fun performStageOperation(nodes: List<GitFileStatusNode>, operation: StagingAreaOperation) {
188 performStageOperation(project, nodes, operation)
191 override fun getDndOperation(targetKind: NodeKind): StagingAreaOperation? {
192 return when (targetKind) {
193 NodeKind.STAGED -> GitAddOperation
194 NodeKind.UNSTAGED -> GitResetOperation
199 override fun showMergeDialog(conflictedFiles: List<VirtualFile>) {
200 AbstractVcsHelper.getInstance(project).showMergeDialog(conflictedFiles)
203 private fun getConflictsToMerge(mergeHandler: GitMergeHandler): List<GitConflict> {
204 return StreamEx.of(selectedStatusNodes()).mapNotNull { it.createConflict() }.filter { conflict ->
205 mergeHandler.canResolveConflict(conflict) && !getConflictOperationLock(myProject, conflict).isLocked
210 private inner class MyGitCommitPanel : GitCommitPanel(project, this) {
211 override fun isFocused(): Boolean {
212 return IdeFocusManager.getInstance(project).getFocusedDescendantFor(this@GitStagePanel) != null
215 override fun performCommit() {
216 performCommit(isAmend)
219 override fun rootsToCommit() = state.stagedRoots.map { VcsRoot(GitVcs.getInstance(project), it) }
222 private inner class MyGitStageTrackerListener : GitStageTrackerListener {
223 override fun update() {
224 this@GitStagePanel.update()
228 private inner class MyGitChangeProviderListener : GitChangeProvider.ChangeProviderListener {
229 override fun progressStarted() {
230 runInEdt(this@GitStagePanel) {
231 tree.setEmptyText(GitBundle.message("stage.loading.status"))
232 progressStripe.startLoading()
236 override fun progressStopped() {
237 runInEdt(this@GitStagePanel) {
238 progressStripe.stopLoading()
239 tree.setEmptyText("")
243 override fun repositoryUpdated(repository: GitRepository) = Unit
246 private inner class MyCommitListener(private val commitMessage: String) : CommitListener {
247 private val notifier = VcsNotifier.getInstance(project)
249 override fun commitProcessFinished(successfulRoots: Collection<VirtualFile>, failedRoots: Map<VirtualFile, VcsException>) {
250 commitFinished(successfulRoots.isNotEmpty() && failedRoots.isEmpty())
252 if (successfulRoots.isNotEmpty()) {
253 notifier.notifySuccess(GitBundle.message("stage.commit.successful", successfulRoots.joinToString {
254 "'${VcsImplUtil.getShortVcsRootName(project, it)}'"
255 }, XmlStringUtil.escapeString(commitMessage)))
257 if (failedRoots.isNotEmpty()) {
258 notifier.notifyError(GitBundle.message("stage.commit.failed", failedRoots.keys.joinToString {
259 "'${VcsImplUtil.getShortVcsRootName(project, it)}'"
260 }), failedRoots.values.joinToString(UIUtil.BR) { it.localizedMessage })
266 internal fun Project.isReversedRoot(root: VirtualFile): Boolean {
267 return GitRepositoryManager.getInstance(this).getRepositoryForRootQuick(root)?.let { repository ->
268 GitMergeUtil.isReverseRoot(repository)
272 internal fun createMergeHandler(project: Project) = GitMergeHandler(project, GitDefaultMergeDialogCustomizer(project))