959d8a4193b921ecd72ee6aa1297fc6ec42071c7
[idea/community.git] / plugins / git4idea / src / git4idea / index / ui / GitStagePanel.kt
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
3
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
53
54 val GIT_STAGE_TRACKER = DataKey.create<GitStageTracker>("GitStageTracker")
55
56 internal class GitStagePanel(private val tracker: GitStageTracker, disposableParent: Disposable) :
57   JPanel(BorderLayout()), DataProvider, Disposable {
58   private val project = tracker.project
59
60   private val tree: GitStageTree
61   private val commitPanel: GitCommitPanel
62   private val progressStripe: ProgressStripe
63
64   private val state: GitStageTracker.State
65     get() = tracker.state
66
67   private var isCommitInProgress = false
68   private var hasPendingUpdates = false
69
70   init {
71     tree = MyChangesTree(project)
72
73     commitPanel = MyGitCommitPanel()
74     commitPanel.createCommitAction().registerCustomShortcutSet(getDefaultCommitShortcut(), this)
75
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)
84
85     PopupHandler.installPopupHandler(tree, "Git.Stage.Tree.Menu", "Git.Stage.Tree.Menu")
86
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
92
93     val leftPanel = JPanel(BorderLayout())
94     leftPanel.add(toolbar.component, BorderLayout.NORTH)
95     leftPanel.add(treeMessageSplitter, BorderLayout.CENTER)
96
97     val diffPreview = GitStageDiffPreview(project, tree, tracker, this)
98     diffPreview.getToolbarWrapper().setVerticalSizeReferent(toolbar.component)
99
100     val commitDiffSplitter = OnePixelSplitter("git.stage.commit.diff.splitter", 0.5f)
101     commitDiffSplitter.firstComponent = leftPanel
102     commitDiffSplitter.secondComponent = diffPreview.component
103
104     add(commitDiffSplitter, BorderLayout.CENTER)
105
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()
111     }
112
113     Disposer.register(disposableParent, this)
114
115     runInEdtAsync(this, { tree.rebuildTree() })
116   }
117
118   private fun performCommit(amend: Boolean) {
119     val rootsToCommit = state.stagedRoots
120     if (rootsToCommit.isEmpty()) return
121
122     val commitMessage = commitPanel.commitMessage.text
123     if (commitMessage.isBlank() && !showEmptyCommitMessageConfirmation()) return
124
125     commitStarted()
126
127     FileDocumentManager.getInstance().saveAllDocuments()
128     git4idea.index.performCommit(project, rootsToCommit, commitMessage, amend, MyCommitListener(commitMessage))
129   }
130
131   @CalledInAwt
132   private fun commitStarted() {
133     isCommitInProgress = true
134     commitPanel.commitButton.isEnabled = false
135   }
136
137   @CalledInAwt
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
144       update()
145     }
146   }
147
148   @CalledInAwt
149   fun update() {
150     if (isCommitInProgress) {
151       hasPendingUpdates = true
152       return
153     }
154     tree.update()
155     commitPanel.commitButton.isEnabled = state.hasStagedRoots()
156   }
157
158   override fun getData(dataId: String): Any? {
159     if (GIT_STAGE_TRACKER.`is`(dataId)) return tracker
160     return null
161   }
162
163   override fun dispose() {
164   }
165
166   private inner class MyChangesTree(project: Project) : GitStageTree(project, this) {
167     override val state
168       get() = this@GitStagePanel.state
169     override val operations: List<StagingAreaOperation> = listOf(GitAddOperation, GitResetOperation)
170
171     init {
172       doubleClickHandler = Processor { e ->
173         if (EditSourceOnDoubleClickHandler.isToggleEvent(this, e)) return@Processor false
174
175         val mergeHandler = createMergeHandler(myProject)
176         val conflicts = getConflictsToMerge(mergeHandler)
177         if (conflicts.isEmpty()) {
178           OpenSourceUtil.openSourcesFrom(DataManager.getInstance().getDataContext(this), true)
179         }
180         else {
181           showMergeWindow(project, mergeHandler, conflicts, myProject::isReversedRoot)
182         }
183         true
184       }
185     }
186
187     override fun performStageOperation(nodes: List<GitFileStatusNode>, operation: StagingAreaOperation) {
188       performStageOperation(project, nodes, operation)
189     }
190
191     override fun getDndOperation(targetKind: NodeKind): StagingAreaOperation? {
192       return when (targetKind) {
193         NodeKind.STAGED -> GitAddOperation
194         NodeKind.UNSTAGED -> GitResetOperation
195         else -> null
196       }
197     }
198
199     override fun showMergeDialog(conflictedFiles: List<VirtualFile>) {
200       AbstractVcsHelper.getInstance(project).showMergeDialog(conflictedFiles)
201     }
202
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
206       }.toList()
207     }
208   }
209
210   private inner class MyGitCommitPanel : GitCommitPanel(project, this) {
211     override fun isFocused(): Boolean {
212       return IdeFocusManager.getInstance(project).getFocusedDescendantFor(this@GitStagePanel) != null
213     }
214
215     override fun performCommit() {
216       performCommit(isAmend)
217     }
218
219     override fun rootsToCommit() = state.stagedRoots.map { VcsRoot(GitVcs.getInstance(project), it) }
220   }
221
222   private inner class MyGitStageTrackerListener : GitStageTrackerListener {
223     override fun update() {
224       this@GitStagePanel.update()
225     }
226   }
227
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()
233       }
234     }
235
236     override fun progressStopped() {
237       runInEdt(this@GitStagePanel) {
238         progressStripe.stopLoading()
239         tree.setEmptyText("")
240       }
241     }
242
243     override fun repositoryUpdated(repository: GitRepository) = Unit
244   }
245
246   private inner class MyCommitListener(private val commitMessage: String) : CommitListener {
247     private val notifier = VcsNotifier.getInstance(project)
248
249     override fun commitProcessFinished(successfulRoots: Collection<VirtualFile>, failedRoots: Map<VirtualFile, VcsException>) {
250       commitFinished(successfulRoots.isNotEmpty() && failedRoots.isEmpty())
251
252       if (successfulRoots.isNotEmpty()) {
253         notifier.notifySuccess(GitBundle.message("stage.commit.successful", successfulRoots.joinToString {
254           "'${VcsImplUtil.getShortVcsRootName(project, it)}'"
255         }, XmlStringUtil.escapeString(commitMessage)))
256       }
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 })
261       }
262     }
263   }
264 }
265
266 internal fun Project.isReversedRoot(root: VirtualFile): Boolean {
267   return GitRepositoryManager.getInstance(this).getRepositoryForRootQuick(root)?.let { repository ->
268     GitMergeUtil.isReverseRoot(repository)
269   } ?: false
270 }
271
272 internal fun createMergeHandler(project: Project) = GitMergeHandler(project, GitDefaultMergeDialogCustomizer(project))