diff --git a/platform/vcs-tests/src/com/intellij/vcs/test/VcsPlatformTest.kt b/platform/vcs-tests/src/com/intellij/vcs/test/VcsPlatformTest.kt index 77482b6d9ae2..2e3acd4c7c59 100644 --- a/platform/vcs-tests/src/com/intellij/vcs/test/VcsPlatformTest.kt +++ b/platform/vcs-tests/src/com/intellij/vcs/test/VcsPlatformTest.kt @@ -199,8 +199,8 @@ abstract class VcsPlatformTest : HeavyPlatformTestCase() { return assertHasNotification(NotificationType.WARNING, title, message, vcsNotifier.notifications) } - protected fun assertErrorNotification(title: String, message: String): Notification { - return assertHasNotification(NotificationType.ERROR, title, message, vcsNotifier.notifications) + protected fun assertErrorNotification(title: String, message: String, actions: List? = null): Notification { + return assertHasNotification(NotificationType.ERROR, title, message, actions, vcsNotifier.notifications) } protected fun assertNoNotification() { diff --git a/plugins/git4idea/resources/messages/GitBundle.properties b/plugins/git4idea/resources/messages/GitBundle.properties index 0ad18e75a3d3..9905b9369b9f 100644 --- a/plugins/git4idea/resources/messages/GitBundle.properties +++ b/plugins/git4idea/resources/messages/GitBundle.properties @@ -30,6 +30,9 @@ apply.changes.operation.canceled={0} canceled apply.changes.operation.failed={0} failed apply.changes.operation.performed.with.conflicts={0} was performed with conflicts apply.changes.operation.successful.for.commits=However, {0} succeeded for the following {1,choice,1#commit|2#commits}: +apply.changes.restore.notification.description=Local changes were saved before {0} +apply.changes.restore.notification.title=Restore local changes +apply.changes.save.and.retry.operation={0} Changes and Retry apply.changes.skipped={0} {1,choice,1#was|2#were} skipped, because all changes have already been {2}. apply.changes.everything.applied=All changes from {0} have already been {1} diff --git a/plugins/git4idea/src/git4idea/GitApplyChangesProcess.kt b/plugins/git4idea/src/git4idea/GitApplyChangesProcess.kt index f15954c4a6ae..ffabae6c15cd 100644 --- a/plugins/git4idea/src/git4idea/GitApplyChangesProcess.kt +++ b/plugins/git4idea/src/git4idea/GitApplyChangesProcess.kt @@ -8,6 +8,7 @@ import com.intellij.history.LocalHistory import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsContexts @@ -15,7 +16,9 @@ import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vcs.AbstractVcsHelper import com.intellij.openapi.vcs.VcsApplicationSettings +import com.intellij.openapi.vcs.VcsBundle import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.VcsNotificationIdsHolder import com.intellij.openapi.vcs.VcsNotifier import com.intellij.openapi.vcs.changes.* import com.intellij.openapi.vcs.update.RefreshVFsSynchronously @@ -27,6 +30,8 @@ import com.intellij.xml.util.XmlStringUtil.wrapInHtml import com.intellij.xml.util.XmlStringUtil.wrapInHtmlTag import git4idea.GitUtil.refreshChangedVfs import git4idea.actions.GitAbortOperationAction +import git4idea.applyChanges.GitApplyChangesLocalChangesDetectedNotification +import git4idea.applyChanges.GitApplyChangesNotificationsHandler import git4idea.changes.GitChangeUtils.getStagedChanges import git4idea.cherrypick.GitLocalChangesConflictDetector import git4idea.commands.GitCommandResult @@ -34,7 +39,6 @@ import git4idea.commands.GitLineHandlerListener import git4idea.commands.GitSimpleEventDetector import git4idea.commands.GitSimpleEventDetector.Event.CHERRY_PICK_CONFLICT import git4idea.commands.GitUntrackedFilesOverwrittenByOperationDetector -import git4idea.config.GitVcsSettings import git4idea.i18n.GitBundle import git4idea.index.isStagingAreaAvailable import git4idea.index.showStagingArea @@ -42,6 +46,7 @@ import git4idea.merge.GitConflictResolver import git4idea.merge.GitDefaultMergeDialogCustomizer import git4idea.repo.GitRepository import git4idea.repo.GitRepositoryManager +import git4idea.stash.GitChangesSaver import git4idea.util.GitUntrackedFilesHelper import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls @@ -69,6 +74,7 @@ internal abstract class GitApplyChangesProcess( private val vcsNotifier = VcsNotifier.getInstance(project) private val changeListManager = ChangeListManagerEx.getInstanceEx(project) private val vcsHelper = AbstractVcsHelper.getInstance(project) + private val notificationsHandler = project.service() protected val autoCommit = forceAutoCommit || !changeListManager.areChangeListsEnabled() protected abstract fun isEmptyCommit(result: GitCommandResult): Boolean @@ -84,6 +90,11 @@ internal abstract class GitApplyChangesProcess( ): GitCommandResult fun execute() { + notificationsHandler.beforeApply() + execute(null, commits) + } + + private fun execute(changesSaver: GitChangesSaver?, commits: List) { // ensure there are no stall changes (ex: from recent commit) that prevent changes from being moved into temp changelist if (changeListManager.areChangeListsEnabled()) { changeListManager.waitForUpdate() @@ -92,30 +103,44 @@ internal abstract class GitApplyChangesProcess( val commitsInRoots = DvcsUtil.groupCommitsByRoots(repositoryManager, commits) LOG.info("${operationName}ing commits: " + toString(commitsInRoots)) + if (changesSaver != null) { + if (!trySaveChanges(commitsInRoots.map { (repo, _) -> repo.root }, changesSaver)) { + return + } + } + val successfulCommits = mutableListOf() val skippedCommits = mutableListOf() for ((repository, repoCommits) in commitsInRoots) { - val success = executeForRepository(repository, repoCommits, successfulCommits, skippedCommits) - if (!success) return + try { + for (commit in repoCommits) { + if (!executeForCommit(repository, commit, successfulCommits, skippedCommits)) { + notificationsHandler.operationFailed(operationName, repository, changesSaver) + return + } + } + } + finally { + repository.update() + } } + notifyResult(successfulCommits, skippedCommits) + if (changesSaver != null) { + LOG.info("Restoring saved changes after successful $operationName") + changesSaver.load() + } } - private fun executeForRepository(repository: GitRepository, - repoCommits: List, - successfulCommits: MutableList, - skippedCommits: MutableList): Boolean { - try { - for (commit in repoCommits) { - val success = executeForCommit(repository, commit, successfulCommits, skippedCommits) - if (!success) return false - } - return true - } - finally { - repository.update() - } + fun trySaveChanges(roots: List, changesSaver: GitChangesSaver): Boolean { + val errorMessage = changesSaver.saveLocalChangesOrError(roots) ?: return true + + VcsNotifier.getInstance(project) + .notifyError(VcsNotificationIdsHolder.UNCOMMITTED_CHANGES_SAVING_ERROR, + VcsBundle.message("notification.title.couldn.t.save.uncommitted.changes"), + errorMessage) + return false } /** @@ -195,9 +220,7 @@ internal abstract class GitApplyChangesProcess( return false } else if (localChangesOverwrittenDetector.isDetected) { - val savingStrategy = GitVcsSettings.getInstance(project).saveChangesPolicy - val message = GitBundle.message("warning.your.local.changes.would.be.overwritten.by", operationName, savingStrategy.text.lowercase()) - notifyError(message, commit, successfulCommits) + handleLocalChangesDetected(repository, commit.takeIf { localChangesOverwrittenDetector.byMerge }, successfulCommits, alreadyPicked) return false } else if (isEmptyCommit(result)) { @@ -215,6 +238,23 @@ internal abstract class GitApplyChangesProcess( } } + private fun handleLocalChangesDetected( + repository: GitRepository, + failedOnCommit: VcsCommitMetadata?, + successfulCommits: MutableList, + alreadyPicked: MutableList, + ) { + val notification = GitApplyChangesLocalChangesDetectedNotification(operationName, failedOnCommit, successfulCommits, repository) { saver -> + val alreadyPickedSet = buildSet { + addAll(alreadyPicked) + addAll(successfulCommits) + } + LOG.info("Re-trying $operationName, skipping ${alreadyPickedSet.size} already processed commits") + execute(saver, commits.filter { commit -> !alreadyPickedSet.contains(commit) }) + } + vcsNotifier.notify(notification) + } + private abstract class CommitStrategy { open fun start() = Unit open fun finish() = Unit @@ -455,16 +495,8 @@ internal abstract class GitApplyChangesProcess( } @Nls - private fun getSuccessfulCommitDetailsIfAny(successfulCommits: List): String { - var description = "" - if (successfulCommits.isNotEmpty()) { - description += UIUtil.HR + - GitBundle.message("apply.changes.operation.successful.for.commits", operationName, successfulCommits.size) + - UIUtil.BR - description += getCommitsDetails(successfulCommits) - } - return description - } + private fun getSuccessfulCommitDetailsIfAny(successfulCommits: List) = + getSuccessfulCommitDetailsIfAny(successfulCommits, operationName) @Nls private fun formSkippedDescription(skipped: List, but: Boolean): String { @@ -475,21 +507,6 @@ internal abstract class GitApplyChangesProcess( return GitBundle.message("apply.changes.everything.applied", hashes, appliedWord) } - @NlsSafe - private fun getCommitsDetails(successfulCommits: List): String { - var description = "" - for (commit in successfulCommits) { - if (description.isNotEmpty()) description += UIUtil.BR - description += commitDetails(commit) - } - return description - } - - @NlsSafe - private fun commitDetails(commit: VcsCommitMetadata): String { - return commit.id.toShortString() + " " + StringUtil.escapeXmlEntities(commit.subject) - } - private fun toString(commitsInRoots: Map>): String { return commitsInRoots.entries.joinToString("; ") { entry -> val commits = entry.value.joinToString { it.id.asString() } @@ -510,8 +527,35 @@ internal abstract class GitApplyChangesProcess( } } - companion object { + internal companion object { private val LOG = logger() + + @NlsSafe + fun commitDetails(commit: VcsCommitMetadata): String { + return commit.id.toShortString() + " " + StringUtil.escapeXmlEntities(commit.subject) + } + + @Nls + fun getSuccessfulCommitDetailsIfAny(successfulCommits: List, operationName: String): String { + var description = "" + if (successfulCommits.isNotEmpty()) { + description += UIUtil.HR + + GitBundle.message("apply.changes.operation.successful.for.commits", operationName, successfulCommits.size) + + UIUtil.BR + description += getCommitsDetails(successfulCommits) + } + return description + } + + @NlsSafe + private fun getCommitsDetails(successfulCommits: List): String { + var description = "" + for (commit in successfulCommits) { + if (description.isNotEmpty()) description += UIUtil.BR + description += commitDetails(commit) + } + return description + } } } diff --git a/plugins/git4idea/src/git4idea/GitNotificationIdsHolder.kt b/plugins/git4idea/src/git4idea/GitNotificationIdsHolder.kt index 0234d316f756..87f85735fb56 100644 --- a/plugins/git4idea/src/git4idea/GitNotificationIdsHolder.kt +++ b/plugins/git4idea/src/git4idea/GitNotificationIdsHolder.kt @@ -9,6 +9,7 @@ class GitNotificationIdsHolder : NotificationIdsHolder { APPLY_CHANGES_SUCCESS, APPLY_CHANGES_CONFLICTS, APPLY_CHANGES_ERROR, + APPLY_CHANGES_LOCAL_CHANGES_DETECTED, BRANCH_UPDATE_FORCE_PUSHED_BRANCH_NOT_ALL_CHERRY_PICKED, BRANCH_UPDATE_FORCE_PUSHED_BRANCH_SUCCESS, BRANCH_CHECKOUT_FAILED, @@ -123,6 +124,7 @@ class GitNotificationIdsHolder : NotificationIdsHolder { const val APPLY_CHANGES_SUCCESS = "git.apply.changes.success" const val APPLY_CHANGES_CONFLICTS = "git.apply.changes.conflicts" const val APPLY_CHANGES_ERROR = "git.apply.changes.error" + const val APPLY_CHANGES_LOCAL_CHANGES_DETECTED = "git.apply.changes.local.changes.detected" const val BRANCH_UPDATE_FORCE_PUSHED_BRANCH_NOT_ALL_CHERRY_PICKED = "git.update.force.pushed.branch.not.all.cherry.picked" const val BRANCH_UPDATE_FORCE_PUSHED_BRANCH_SUCCESS = "git.update.force.pushed.branch.success" const val BRANCH_CHECKOUT_FAILED = "git.branch.checkout.failed" diff --git a/plugins/git4idea/src/git4idea/GitRestoreSavedChangesNotificationAction.kt b/plugins/git4idea/src/git4idea/GitRestoreSavedChangesNotificationAction.kt new file mode 100644 index 000000000000..8c7d20742fda --- /dev/null +++ b/plugins/git4idea/src/git4idea/GitRestoreSavedChangesNotificationAction.kt @@ -0,0 +1,19 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea + +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.openapi.actionSystem.AnActionEvent +import git4idea.i18n.GitBundle +import git4idea.stash.GitChangesSaver + +internal class GitRestoreSavedChangesNotificationAction(private val saver: GitChangesSaver) : NotificationAction( + saver.saveMethod.selectBundleMessage( + GitBundle.message("rebase.notification.action.view.stash.text"), + GitBundle.message("rebase.notification.action.view.shelf.text") + ) +) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + saver.showSavedChanges() + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesCanRestoreNotification.kt b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesCanRestoreNotification.kt new file mode 100644 index 000000000000..3443790f454b --- /dev/null +++ b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesCanRestoreNotification.kt @@ -0,0 +1,44 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.applyChanges + +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationType +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.VcsBundle +import com.intellij.openapi.vcs.VcsNotifier +import com.intellij.platform.ide.progress.withBackgroundProgress +import git4idea.GitApplyChangesNotification +import git4idea.GitDisposable +import git4idea.GitRestoreSavedChangesNotificationAction +import git4idea.i18n.GitBundle +import git4idea.stash.GitChangesSaver +import kotlinx.coroutines.launch +import org.jetbrains.annotations.Nls + +internal class GitApplyChangesCanRestoreNotification( + project: Project, + changesSaver: GitChangesSaver, + operationName: @Nls String, +) : GitApplyChangesNotification( + VcsNotifier.importantNotification().displayId, + GitBundle.message("apply.changes.restore.notification.title"), + GitBundle.message("apply.changes.restore.notification.description", operationName), + NotificationType.INFORMATION, +) { + init { + addAction(GitRestoreSavedChangesNotificationAction(changesSaver)) + addAction(NotificationAction.createExpiring(changesSaver.saveMethod.selectBundleMessage( + GitBundle.message("unstash.title"), + VcsBundle.message("unshelve.changes.action") + )) { _, _ -> + GitDisposable.getInstance(project).coroutineScope.launch { + withBackgroundProgress(project, changesSaver.saveMethod.selectBundleMessage( + GitBundle.message("unstash.unstashing"), + VcsBundle.message("unshelve.changes.progress.title") + )) { + changesSaver.load() + } + } + }) + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesLocalChangesDetectedNotification.kt b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesLocalChangesDetectedNotification.kt new file mode 100644 index 000000000000..f1364ae5ef6f --- /dev/null +++ b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesLocalChangesDetectedNotification.kt @@ -0,0 +1,112 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.applyChanges + +import com.intellij.ide.IdeBundle +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationType +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.VcsBundle +import com.intellij.openapi.vcs.VcsNotificationIdsHolder +import com.intellij.openapi.vcs.VcsNotifier +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.util.ui.UIUtil +import com.intellij.vcs.log.VcsCommitMetadata +import git4idea.GitApplyChangesNotification +import git4idea.GitApplyChangesProcess +import git4idea.GitDisposable +import git4idea.GitNotificationIdsHolder +import git4idea.GitUtil +import git4idea.commands.Git +import git4idea.config.GitSaveChangesPolicy +import git4idea.config.GitVcsApplicationSettings +import git4idea.config.GitVcsSettings +import git4idea.i18n.GitBundle +import git4idea.repo.GitRepository +import git4idea.stash.GitChangesSaver +import git4idea.util.LocalChangesWouldBeOverwrittenHelper +import kotlinx.coroutines.launch +import org.jetbrains.annotations.Nls + +internal class GitApplyChangesLocalChangesDetectedNotification( + operationName: @Nls String, + failedOnCommit: VcsCommitMetadata?, + successfulCommits: List, + repository: GitRepository, + retryAction: (GitChangesSaver) -> Unit, +) : GitApplyChangesNotification( + VcsNotifier.importantNotification().displayId, + GitBundle.message("apply.changes.operation.failed", operationName.capitalize()), + getDescription(operationName, repository, failedOnCommit, successfulCommits), + NotificationType.ERROR, +) { + init { + val project = repository.project + val affectedPaths = repository.getStagingAreaHolder().allRecords.map { it.path } + val localChanges = + GitUtil.findLocalChangesForPaths(project, repository.getRoot(), affectedPaths.map(FilePath::getPath), false) + + setDisplayId(GitNotificationIdsHolder.Companion.APPLY_CHANGES_LOCAL_CHANGES_DETECTED) + + addAction(NotificationAction.createSimple(IdeBundle.messagePointer("action.show.files")) { + LocalChangesWouldBeOverwrittenHelper.showErrorDialog( + project, + operationName, + null, + localChanges, + affectedPaths.map { it.path } + ) + }) + + if (localChanges.isNotEmpty()) { + addAction(saveAndRetryAction(repository, operationName, retryAction)) + } + } + + private fun saveAndRetryAction( + repository: GitRepository, + operationName: @Nls String, + retryAction: (GitChangesSaver) -> Unit, + ): NotificationAction { + val savingStrategy = getSavingStrategy(repository.project) + val actionText = GitBundle.message("apply.changes.save.and.retry.operation", savingStrategy.text) + return NotificationAction.createExpiring(actionText) { _, _ -> + GitDisposable.getInstance(repository.project).coroutineScope.launch { + withBackgroundProgress(repository.project, savingStrategy.selectBundleMessage( + GitBundle.message("stashing.progress.title"), + VcsBundle.message("shelve.changes.progress.text") + )) { + val changesSaver = GitChangesSaver.getSaver(repository.project, Git.getInstance(), EmptyProgressIndicator(), + VcsBundle.message("stash.changes.message", operationName), savingStrategy) + retryAction(changesSaver) + } + } + } + } + + private companion object { + fun getSavingStrategy(project: Project) = + if (GitVcsApplicationSettings.getInstance().isStagingAreaEnabled) GitSaveChangesPolicy.STASH + else GitVcsSettings.getInstance(project).saveChangesPolicy + + + fun getDescription( + operationName: @Nls String, + repository: GitRepository, + failedOnCommit: VcsCommitMetadata?, + successfulCommits: List, + ): @Nls String { + var description = if (failedOnCommit != null) { + GitApplyChangesProcess.commitDetails(failedOnCommit) + UIUtil.BR + } + else "" + + description += GitBundle.message("warning.your.local.changes.would.be.overwritten.by", operationName, + StringUtil.toLowerCase(getSavingStrategy(repository.project).text)) + description += GitApplyChangesProcess.getSuccessfulCommitDetailsIfAny(successfulCommits, operationName) + return description + } + } +} diff --git a/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesNotificationsHandler.kt b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesNotificationsHandler.kt new file mode 100644 index 000000000000..acd03269c5ed --- /dev/null +++ b/plugins/git4idea/src/git4idea/applyChanges/GitApplyChangesNotificationsHandler.kt @@ -0,0 +1,80 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.applyChanges + +import com.intellij.dvcs.repo.Repository +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.VcsNotifier +import git4idea.GitApplyChangesNotification +import git4idea.GitDisposable +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryChangeListener +import git4idea.stash.GitChangesSaver +import org.jetbrains.annotations.Nls +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Helper class for hiding or showing notifications related to applying changes + * when [git4idea.GitApplyChangesProcess.execute] is already finished + */ +@Service(Service.Level.PROJECT) +internal class GitApplyChangesNotificationsHandler(private val project: Project) { + private var shouldHideNotifications = AtomicBoolean() + private var changesSaverAndOperation = AtomicReference?>() + + init { + project.messageBus.connect(GitDisposable.Companion.getInstance(project)) + .subscribe(GitRepository.GIT_REPO_CHANGE, GitRepositoryChangeListener { + if (!isCherryPickingOrReverting(it)) { + if (shouldHideNotifications.compareAndSet(true, false)) { + LOG.debug("Hiding notifications") + GitApplyChangesNotification.Companion.expireAll(project) + } + + changesSaverAndOperation.getAndSet(null)?.let { (changesSaver, operation) -> + LOG.debug("Suggesting to restore saved changes after $operation") + showRestoreChangesNotification(changesSaver, operation) + } + } + }) + } + + fun beforeApply() { + shouldHideNotifications.set(false) + changesSaverAndOperation.set(null) + } + + fun operationFailed(operationName: @Nls String, repository: GitRepository, changesSaver: GitChangesSaver?) { + val cherryPickingOrReverting = isCherryPickingOrReverting(repository) + if (cherryPickingOrReverting) { + shouldHideNotifications.set(true) + } + + if (changesSaver != null) { + if (cherryPickingOrReverting) { + changesSaverAndOperation.set(changesSaver to operationName) + } + else { + showRestoreChangesNotification(changesSaver, operationName) + } + } + } + + private fun showRestoreChangesNotification(changesSaver: GitChangesSaver, operation: @Nls String) { + VcsNotifier.getInstance(project).notify( + GitApplyChangesCanRestoreNotification(project, changesSaver, operation) + ) + } + + private fun isCherryPickingOrReverting(repository: GitRepository): Boolean = + repository.state == Repository.State.GRAFTING || repository.state == Repository.State.REVERTING + + internal companion object { + private val LOG = thisLogger() + + fun getInstance(project: Project): GitApplyChangesNotificationsHandler = + project.getService(GitApplyChangesNotificationsHandler::class.java) + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/cherrypick/CherryPickProcess.kt b/plugins/git4idea/src/git4idea/cherrypick/CherryPickProcess.kt index f0d1daf449e7..3a8c3aa7ad16 100644 --- a/plugins/git4idea/src/git4idea/cherrypick/CherryPickProcess.kt +++ b/plugins/git4idea/src/git4idea/cherrypick/CherryPickProcess.kt @@ -2,7 +2,6 @@ package git4idea.cherrypick import com.intellij.dvcs.ui.DvcsBundle -import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project @@ -43,10 +42,6 @@ internal class GitCherryPickProcess( fun isSuccess() = successfullyCherryPickedCount == totalCommitsToCherryPick - init { - project.service() - } - override fun isEmptyCommit(result: GitCommandResult): Boolean { val stdout = result.outputAsJoinedString val stderr = result.errorOutputAsJoinedString diff --git a/plugins/git4idea/src/git4idea/cherrypick/GitCherryPickNotificationsHandler.kt b/plugins/git4idea/src/git4idea/cherrypick/GitCherryPickNotificationsHandler.kt deleted file mode 100644 index 0bb2a473a286..000000000000 --- a/plugins/git4idea/src/git4idea/cherrypick/GitCherryPickNotificationsHandler.kt +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package git4idea.cherrypick - -import com.intellij.dvcs.repo.Repository -import com.intellij.openapi.components.Service -import com.intellij.openapi.project.Project -import git4idea.GitApplyChangesNotification -import git4idea.GitApplyChangesNotification.ExpireAfterRepoStateChanged -import git4idea.GitDisposable -import git4idea.repo.GitRepository -import git4idea.repo.GitRepositoryChangeListener -import java.util.concurrent.ConcurrentHashMap - -@Service(Service.Level.PROJECT) -internal class GitCherryPickNotificationsHandler(project: Project) { - private val cherryPickingIn = ConcurrentHashMap.newKeySet() - - init { - project.messageBus.connect(GitDisposable.getInstance(project)) - .subscribe(GitRepository.GIT_REPO_CHANGE, GitRepositoryChangeListener { - if (it.state == Repository.State.GRAFTING) { - cherryPickingIn.add(it) - } - else if (cherryPickingIn.remove(it)) { - GitApplyChangesNotification.expireAll(project) - } - }) - } -} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java index d9ddbb58a0e9..b52c782f4ef0 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java @@ -30,10 +30,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; import com.intellij.util.progress.StepsProgressIndicator; import com.intellij.vcs.log.Hash; import com.intellij.vcs.log.TimedVcsCommit; -import git4idea.DialogManager; -import git4idea.GitActivity; -import git4idea.GitNotificationIdsHolder; -import git4idea.GitProtectedBranchesKt; +import git4idea.*; import git4idea.branch.GitRebaseParams; import git4idea.commands.*; import git4idea.config.GitSaveChangesPolicy; @@ -106,13 +103,7 @@ public class GitRebaseProcess { myProgressManager = ProgressManager.getInstance(); myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject); - VIEW_STASH_ACTION = NotificationAction.createSimple( - mySaver.getSaveMethod().selectBundleMessage( - GitBundle.message("rebase.notification.action.view.stash.text"), - GitBundle.message("rebase.notification.action.view.shelf.text") - ), - () -> mySaver.showSavedChanges() - ); + VIEW_STASH_ACTION = new GitRestoreSavedChangesNotificationAction(mySaver); } public void rebase() { diff --git a/plugins/git4idea/src/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java b/plugins/git4idea/src/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java index d057ef844f71..848c9ae112c6 100644 --- a/plugins/git4idea/src/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java +++ b/plugins/git4idea/src/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java @@ -30,22 +30,25 @@ public final class LocalChangesWouldBeOverwrittenHelper { final Collection absolutePaths = GitUtil.toAbsolute(root, relativeFilePaths); final List changes = GitUtil.findLocalChangesForPaths(project, root, absolutePaths, false); + String description = getOverwrittenByMergeMessage(); VcsNotifier.importantNotification() .createNotification(GitBundle.message("notification.title.git.operation.failed", StringUtil.capitalize(operationName)), - GitBundle.message(getOverwrittenByMergeMessage()), + description, NotificationType.ERROR) .setDisplayId(displayId) .addAction(NotificationAction.createSimple( GitBundle.messagePointer("local.changes.would.be.overwritten.by.merge.view.them.action"), () -> { - showErrorDialog(project, operationName, changes, absolutePaths); + showErrorDialog(project, operationName, description, changes, absolutePaths); })) .notify(project); } - private static void showErrorDialog(@NotNull Project project, @NotNull String operationName, @NotNull List changes, - @NotNull Collection absolutePaths) { + public static void showErrorDialog(@NotNull Project project, + @NotNull String operationName, + @Nls String description, + @NotNull List changes, + @NotNull Collection absolutePaths) { String title = GitBundle.message("dialog.title.local.changes.prevent.from.operation", StringUtil.capitalize(operationName)); - String description = GitBundle.message(getOverwrittenByMergeMessage()); if (changes.isEmpty()) { GitUtil.showPathsInDialog(project, absolutePaths, title, description); } @@ -53,7 +56,9 @@ public final class LocalChangesWouldBeOverwrittenHelper { ChangesBrowserWithRollback changesViewer = new ChangesBrowserWithRollback(project, changes); DialogBuilder builder = new DialogBuilder(project); - builder.setNorthPanel(new MultiLineLabel(description)); + if (description != null) { + builder.setNorthPanel(new MultiLineLabel(description)); + } builder.setCenterPanel(changesViewer); builder.addDisposable(changesViewer); builder.addOkAction(); diff --git a/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickAutoCommitTest.kt b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickAutoCommitTest.kt index 2fe05bf1eefe..82b2c5ae6341 100644 --- a/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickAutoCommitTest.kt +++ b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickAutoCommitTest.kt @@ -1,6 +1,7 @@ // Copyright 2000-2019 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.cherrypick +import com.intellij.ide.IdeBundle import git4idea.i18n.GitBundle import git4idea.test.* import org.junit.Test @@ -194,4 +195,18 @@ class GitCherryPickAutoCommitTest(private val createChangelistAutomatically: Boo ${shortHash(commit3)} fix #2 ${shortHash(emptyCommit)} was skipped, because all changes have already been applied.""") } + + @Test + fun `staged changes prevent cherry-pick`() { + val commit = file("a.txt").create().addCommit("fix #1").hash() + file("b.txt").create().add() + + cherryPick(commit, expectSuccess = false) + + assertErrorNotification("Cherry-pick failed", + GitBundle.message("warning.your.local.changes.would.be.overwritten.by", "cherry-pick", "shelve"), + listOf(IdeBundle.message("action.show.files"), + GitBundle.message("apply.changes.save.and.retry.operation", "Shelve")) + ) + } } \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.kt b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.kt index febeb6e67e78..fb6387528846 100644 --- a/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.kt +++ b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.kt @@ -15,7 +15,10 @@ */ package git4idea.cherrypick +import com.intellij.ide.IdeBundle import com.intellij.openapi.vcs.VcsApplicationSettings +import com.intellij.util.ui.Html +import com.intellij.util.ui.UIUtil import com.intellij.vcs.log.impl.HashImpl import git4idea.i18n.GitBundle import git4idea.test.* @@ -38,9 +41,12 @@ abstract class GitCherryPickTest : GitSingleRepoTest() { cherryPick(commit, expectSuccess = false) - assertErrorNotification("Cherry-pick failed", """ - ${shortHash(commit)} fix #1 - """ + GitBundle.message("warning.your.local.changes.would.be.overwritten.by", "cherry-pick", "shelve")) + assertErrorNotification("Cherry-pick failed", + "${shortHash(commit)} fix #1" + + UIUtil.BR + + GitBundle.message("warning.your.local.changes.would.be.overwritten.by", "cherry-pick", "shelve"), + listOf(IdeBundle.message("action.show.files"), + GitBundle.message("apply.changes.save.and.retry.operation", "Shelve"))) } protected fun `check untracked file conflicting with commit`() { diff --git a/plugins/git4idea/tests/git4idea/revert/GitRevertTest.kt b/plugins/git4idea/tests/git4idea/revert/GitRevertTest.kt index 2651be7c5cd5..44c945d4db24 100644 --- a/plugins/git4idea/tests/git4idea/revert/GitRevertTest.kt +++ b/plugins/git4idea/tests/git4idea/revert/GitRevertTest.kt @@ -15,6 +15,7 @@ */ package git4idea.revert +import com.intellij.ide.IdeBundle import com.intellij.openapi.vcs.VcsApplicationSettings import com.intellij.openapi.vcs.changes.Change import com.intellij.vcs.log.VcsFullCommitDetails @@ -25,6 +26,7 @@ import git4idea.GitRevisionNumber import git4idea.history.GitHistoryUtils import git4idea.i18n.GitBundle import git4idea.test.* +import org.junit.Test import java.nio.charset.Charset /** @@ -259,6 +261,19 @@ class GitRevertTest : GitSingleRepoTest() { } } + fun `test staged changes prevent revert with auto-commit`() { + val commit = file("a.txt").create().addCommit("fix #1").details() + file("c.txt").create().add() + + revertAutoCommit(commit) + + assertErrorNotification("Revert failed", + GitBundle.message("warning.your.local.changes.would.be.overwritten.by", "revert", "shelve"), + listOf(IdeBundle.message("action.show.files"), + GitBundle.message("apply.changes.save.and.retry.operation", "Shelve")) + ) + } + private fun commitMessageForRevert(commit: VcsFullCommitDetails): String { return """ Revert "${commit.subject}"