diff --git a/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties b/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties index 48036e37b3bf..2dfa2b06112e 100644 --- a/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties +++ b/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties @@ -16,6 +16,7 @@ vcs.revision.name.current=Current message.text.could.not.load.file.content=Could Not Load File Content confirmation.text.check.in.with.empty.comment=Commit with empty comment? confirmation.title.check.in.with.empty.comment=Comment Is Empty +commit.message.placeholder=Commit Message label.commit.comment=&Commit Message border.standard.checkin.options.group=Before {0} border.standard.after.checkin.options.group=After {0} diff --git a/platform/vcs-impl/src/com/intellij/vcs/commit/ChangesViewCommitPanel.kt b/platform/vcs-impl/src/com/intellij/vcs/commit/ChangesViewCommitPanel.kt index fcbf5bf9976d..ed342864b6e5 100644 --- a/platform/vcs-impl/src/com/intellij/vcs/commit/ChangesViewCommitPanel.kt +++ b/platform/vcs-impl/src/com/intellij/vcs/commit/ChangesViewCommitPanel.kt @@ -64,7 +64,13 @@ private val MAC_COMMIT_ACTION_SHORTCUT = CustomShortcutSet(getKeyStroke(KeyEvent private fun panel(layout: LayoutManager): JBPanel<*> = JBPanel>(layout) -private fun JBOptionButton.getBottomInset(): Int = +fun showEmptyCommitMessageConfirmation() = Messages.YES == Messages.showYesNoDialog( + message("confirmation.text.check.in.with.empty.comment"), + message("confirmation.title.check.in.with.empty.comment"), + Messages.getWarningIcon() +) + +fun JBOptionButton.getBottomInset(): Int = border?.getBorderInsets(this)?.bottom ?: (components.firstOrNull() as? JComponent)?.insets?.bottom ?: 0 @@ -118,7 +124,7 @@ open class ChangesViewCommitPanel(private val changesView: ChangesListView, priv private val commitMessage = CommitMessage(project, false, false, true).apply { editorField.addSettingsProvider { it.setBorder(emptyLeft(6)) } - editorField.setPlaceholder("Commit Message") + editorField.setPlaceholder(message("commit.message.placeholder")) } private val defaultCommitAction = object : AbstractAction() { override fun actionPerformed(e: ActionEvent) = fireDefaultExecutorCalled() @@ -383,12 +389,7 @@ open class ChangesViewCommitPanel(private val changesView: ChangesListView, priv override fun addInclusionListener(listener: InclusionListener, parent: Disposable) = inclusionEventDispatcher.addListener(listener, parent) - override fun confirmCommitWithEmptyMessage(): Boolean = - Messages.YES == Messages.showYesNoDialog( - message("confirmation.text.check.in.with.empty.comment"), - message("confirmation.title.check.in.with.empty.comment"), - Messages.getWarningIcon() - ) + override fun confirmCommitWithEmptyMessage(): Boolean = showEmptyCommitMessageConfirmation() override fun startBeforeCommitChecks() = Unit override fun endBeforeCommitChecks(result: CheckinHandler.ReturnResult) = Unit diff --git a/plugins/git4idea/resources/messages/GitBundle.properties b/plugins/git4idea/resources/messages/GitBundle.properties index 2a9805538cb0..7accdc5d2cea 100644 --- a/plugins/git4idea/resources/messages/GitBundle.properties +++ b/plugins/git4idea/resources/messages/GitBundle.properties @@ -563,6 +563,7 @@ stage.tree.node.unstaged=Not staged for commit stage.tree.node.unmerged=Unmerged stage.tree.node.untracked=Untracked stage.tree.node.ignored=Ignored +stage.commit.process=Committing Staged Changes... stage.loading.status=Loading changes... branches.selected.branches.updated.title=Selected {0,choice,1#branch|2#branches} Updated diff --git a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java index 0d5c7a4ff37a..805e680960da 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java +++ b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java @@ -979,8 +979,8 @@ public class GitCheckinEnvironment implements CheckinEnvironment, AmendCommitAwa return file; } - private static void runWithMessageFile(@NotNull Project project, @NotNull VirtualFile root, @NotNull String message, - @NotNull ThrowableConsumer task) throws VcsException { + public static void runWithMessageFile(@NotNull Project project, @NotNull VirtualFile root, @NotNull String message, + @NotNull ThrowableConsumer task) throws VcsException { File messageFile; try { messageFile = createCommitMessageFile(project, root, message); diff --git a/plugins/git4idea/src/git4idea/index/GitCommitUtil.kt b/plugins/git4idea/src/git4idea/index/GitCommitUtil.kt new file mode 100644 index 000000000000..8679cefe70ae --- /dev/null +++ b/plugins/git4idea/src/git4idea/index/GitCommitUtil.kt @@ -0,0 +1,49 @@ +// 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.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vfs.VirtualFile +import git4idea.checkin.GitCheckinEnvironment +import git4idea.commands.Git +import git4idea.commands.GitCommand +import git4idea.commands.GitLineHandler +import git4idea.i18n.GitBundle +import git4idea.repo.GitRepositoryManager +import java.io.File + +private val LOG = Logger.getInstance("#git4idea.index.GitCommitUtil") + +fun performCommit(project: Project, roots: Collection, commitMessage: String, amend: Boolean = false) { + object : Backgroundable(project, GitBundle.message("stage.commit.process")) { + override fun run(indicator: ProgressIndicator) { + for (root in roots) { + try { + GitCheckinEnvironment.runWithMessageFile(project, root, commitMessage) { commitMessageFile: File -> + doCommit(project, root, commitMessageFile, amend) + } + GitRepositoryManager.getInstance(project).getRepositoryForRoot(root)?.repositoryFiles?.refresh() + } + catch (e: VcsException) { + LOG.error("Error while committing $root", e) + } + } + } + }.queue() +} + +@Throws(VcsException::class) +private fun doCommit(project: Project, root: VirtualFile, commitMessageFile: File, amend: Boolean) { + val handler = GitLineHandler(project, root, GitCommand.COMMIT) + handler.setStdoutSuppressed(false) + handler.addParameters("-F") + handler.addAbsoluteFile(commitMessageFile) + if (amend) { + handler.addParameters("--amend") + } + handler.endOptions() + Git.getInstance().runCommand(handler).getOutputOrThrow() +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/index/GitStageTracker.kt b/plugins/git4idea/src/git4idea/index/GitStageTracker.kt index 0e53b28c91aa..759a8f2c0894 100644 --- a/plugins/git4idea/src/git4idea/index/GitStageTracker.kt +++ b/plugins/git4idea/src/git4idea/index/GitStageTracker.kt @@ -99,6 +99,16 @@ class GitStageTracker(val project: Project) : Disposable { data class State(val gitState: Map>) { private val roots: Collection get() = gitState.keys + val stagedRoots: Set + get() { + return gitState.filterValues { + it.any { line -> line.getStagedStatus() != null } + }.keys + } + + fun hasStagedRoots(): Boolean { + return gitState.values.flatten().any { it.getStagedStatus() != null } + } fun updatedWith(newState: State, roots: Collection = this.roots.union(newState.roots)): State { val gitState = hashMapOf>() diff --git a/plugins/git4idea/src/git4idea/index/ui/GitCommitPanel.kt b/plugins/git4idea/src/git4idea/index/ui/GitCommitPanel.kt new file mode 100644 index 000000000000..a420786ee299 --- /dev/null +++ b/plugins/git4idea/src/git4idea/index/ui/GitCommitPanel.kt @@ -0,0 +1,121 @@ +// 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.idea.ActionsBundle +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.ex.CheckboxAction +import com.intellij.openapi.editor.colors.EditorColorsListener +import com.intellij.openapi.editor.colors.EditorColorsScheme +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComponentContainer +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vcs.VcsBundle +import com.intellij.openapi.vcs.ui.CommitMessage +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBOptionButton +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.panels.HorizontalLayout +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.components.BorderLayoutPanel +import com.intellij.vcs.commit.getBottomInset +import git4idea.i18n.GitBundle +import java.awt.Component +import java.awt.event.ActionEvent +import javax.swing.AbstractAction +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.SwingConstants +import javax.swing.border.Border +import javax.swing.border.EmptyBorder + +abstract class GitCommitPanel(project: Project, + parent: Disposable) : BorderLayoutPanel(), EditorColorsListener, ComponentContainer, DataProvider { + var isAmend: Boolean = false + + val commitMessage = CommitMessage(project, false, false, true) + val commitButton = CommitButton() + + init { + addToCenter(buildPanel()) + withPreferredHeight(85) + + Disposer.register(this, commitMessage) + Disposer.register(parent, this) + } + + private fun buildPanel(): Component { + val centerPanel = JBUI.Panels.simplePanel() + + val amendActionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, + DefaultActionGroup(AmendAction()), true) + amendActionToolbar.setTargetComponent(this) + amendActionToolbar.setReservePlaceAutoPopupIcon(false) + amendActionToolbar.component.isOpaque = false + amendActionToolbar.component.border = null + amendActionToolbar.component.background = getButtonPanelBackground() + + val bottomPanel = JBPanel>(HorizontalLayout(JBUI.scale(4), SwingConstants.CENTER)) + bottomPanel.background = getButtonPanelBackground() + bottomPanel.border = getButtonPanelBorder() + bottomPanel.add(commitButton) + bottomPanel.add(amendActionToolbar.component) + + commitMessage.editorField.addSettingsProvider { it.setBorder(JBUI.Borders.emptyLeft(6)) } + commitMessage.editorField.setPlaceholder(VcsBundle.message("commit.message.placeholder")) + + centerPanel.addToCenter(commitMessage).addToBottom(bottomPanel) + return centerPanel + } + + protected abstract fun performCommit() + + protected abstract fun isFocused(): Boolean + + private fun getButtonPanelBorder(): Border = + EmptyBorder(0, JBUI.scale(4), 0, 0) + + private fun getButtonPanelBackground() = + JBColor { (commitMessage.editorField.editor as? EditorEx)?.backgroundColor ?: UIUtil.getTreeBackground() } + + override fun globalSchemeChange(scheme: EditorColorsScheme?) { + // todo + } + + override fun getComponent(): JComponent = this + + override fun getPreferredFocusableComponent(): JComponent = commitMessage.editorField + + override fun getData(dataId: String) = commitMessage.getData(dataId) + + override fun dispose() { + } + + private fun getCommitText(): String { + if (isAmend) return ActionsBundle.message("action.Vcs.ToggleAmendCommitMode.text") + else return GitBundle.getString("commit.action.name") + } + + inner class CommitButton : JButton(object : AbstractAction(getCommitText()) { + override fun actionPerformed(e: ActionEvent) { + performCommit() + } + }) { + + init { + background = getButtonPanelBackground() + } + + override fun isDefaultButton(): Boolean = isFocused() + } + + inner class AmendAction : CheckboxAction(VcsBundle.messagePointer("checkbox.amend")) { + override fun isSelected(e: AnActionEvent): Boolean = isAmend + override fun setSelected(e: AnActionEvent, state: Boolean) { + isAmend = state + commitButton.text = getCommitText() + } + } +} diff --git a/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt b/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt index cc6a85841a8d..07f0999dcbd1 100644 --- a/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt +++ b/plugins/git4idea/src/git4idea/index/ui/GitStagePanel.kt @@ -8,8 +8,11 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.vcs.changes.ui.ChangesTree import com.intellij.openapi.vcs.changes.ui.TreeActionsToolbarPanel +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.ui.OnePixelSplitter import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.SideBorder +import com.intellij.vcs.commit.showEmptyCommitMessageConfirmation import com.intellij.vcs.log.ui.frame.ProgressStripe import git4idea.i18n.GitBundle import git4idea.index.GitStageTracker @@ -24,6 +27,7 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar private val project = tracker.project private val tree: GitStageTree + private val commitPanel: GitCommitPanel private val progressStripe: ProgressStripe private val state: GitStageTracker.State @@ -31,6 +35,7 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar init { tree = MyChangesTree(project) + commitPanel = MyGitCommitPanel() val toolbarGroup = DefaultActionGroup() toolbarGroup.add(ActionManager.getInstance().getAction("Git.Stage.Toolbar")) @@ -43,8 +48,12 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar val scrolledTree = ScrollPaneFactory.createScrollPane(tree, SideBorder.TOP) progressStripe = ProgressStripe(scrolledTree, this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS) + val treeMessageSplitter = OnePixelSplitter(true, "git.stage.tree.message.splitter", 0.7f) + treeMessageSplitter.firstComponent = progressStripe + treeMessageSplitter.secondComponent = commitPanel + add(toolbar.component, BorderLayout.NORTH) - add(progressStripe, BorderLayout.CENTER) + add(treeMessageSplitter, BorderLayout.CENTER) tracker.addListener(MyGitStageTrackerListener(), this) if (tracker.isRefreshInProgress) { @@ -56,8 +65,18 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar Disposer.register(disposableParent, this) } + private fun performCommit(amend: Boolean) { + val rootsToCommit = state.stagedRoots + if (rootsToCommit.isEmpty()) return + + if (commitPanel.commitMessage.text.isBlank() && !showEmptyCommitMessageConfirmation()) return + + git4idea.index.performCommit(project, rootsToCommit, commitPanel.commitMessage.text, amend) + } + fun update() { tree.update() + commitPanel.commitButton.isEnabled = state.hasStagedRoots() } override fun getData(dataId: String): Any? { @@ -73,6 +92,16 @@ internal class GitStagePanel(private val tracker: GitStageTracker, disposablePar get() = this@GitStagePanel.state } + inner class MyGitCommitPanel : GitCommitPanel(project, this) { + override fun isFocused(): Boolean { + return IdeFocusManager.getInstance(project).getFocusedDescendantFor(this@GitStagePanel) != null + } + + override fun performCommit() { + performCommit(isAmend) + } + } + inner class MyGitStageTrackerListener : GitStageTrackerListener { override fun update() { this@GitStagePanel.update() diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java index c7e953bad960..610dc1cc741a 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java @@ -352,6 +352,15 @@ public class GitRepositoryFiles { return path.equals(myExcludePath); } + /** + * Refresh all .git repository files asynchronously and recursively. + * + * @see #refreshTagsFiles() if you need the "main" data (branches, HEAD, etc.) to be updated synchronously. + */ + public void refresh() { + VfsUtil.markDirtyAndRefresh(true, true, false, myMainDir, myWorktreeDir); + } + /** * Refresh that part of .git repository files, which is not covered by {@link GitRepository#update()}, e.g. the {@code refs/tags/} dir. *