mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
[git] IJPL-73913 Suggest stashing/shelving changes and retry cherry-pick
GitOrigin-RevId: d73345928d536ddfe9f22666b4f2dd3272263c31
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ed0d05e826
commit
37600c5ef0
@@ -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<String>? = null): Notification {
|
||||
return assertHasNotification(NotificationType.ERROR, title, message, actions, vcsNotifier.notifications)
|
||||
}
|
||||
|
||||
protected fun assertNoNotification() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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<GitApplyChangesNotificationsHandler>()
|
||||
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<VcsCommitMetadata>) {
|
||||
// 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<VcsCommitMetadata>()
|
||||
val skippedCommits = mutableListOf<VcsCommitMetadata>()
|
||||
|
||||
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<VcsCommitMetadata>,
|
||||
successfulCommits: MutableList<VcsCommitMetadata>,
|
||||
skippedCommits: MutableList<VcsCommitMetadata>): 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<VirtualFile>, 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<VcsCommitMetadata>,
|
||||
alreadyPicked: MutableList<VcsCommitMetadata>,
|
||||
) {
|
||||
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<VcsCommitMetadata>): 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<VcsCommitMetadata>) =
|
||||
getSuccessfulCommitDetailsIfAny(successfulCommits, operationName)
|
||||
|
||||
@Nls
|
||||
private fun formSkippedDescription(skipped: List<VcsCommitMetadata>, 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<VcsCommitMetadata>): 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<GitRepository, List<VcsCommitMetadata>>): 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<GitApplyChangesProcess>()
|
||||
|
||||
@NlsSafe
|
||||
fun commitDetails(commit: VcsCommitMetadata): String {
|
||||
return commit.id.toShortString() + " " + StringUtil.escapeXmlEntities(commit.subject)
|
||||
}
|
||||
|
||||
@Nls
|
||||
fun getSuccessfulCommitDetailsIfAny(successfulCommits: List<VcsCommitMetadata>, 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<VcsCommitMetadata>): String {
|
||||
var description = ""
|
||||
for (commit in successfulCommits) {
|
||||
if (description.isNotEmpty()) description += UIUtil.BR
|
||||
description += commitDetails(commit)
|
||||
}
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<VcsCommitMetadata>,
|
||||
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<VcsCommitMetadata>,
|
||||
): @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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Pair<GitChangesSaver, @Nls String>?>()
|
||||
|
||||
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<GitApplyChangesNotification.ExpireAfterRepoStateChanged>(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)
|
||||
}
|
||||
}
|
||||
@@ -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<GitCherryPickNotificationsHandler>()
|
||||
}
|
||||
|
||||
override fun isEmptyCommit(result: GitCommandResult): Boolean {
|
||||
val stdout = result.outputAsJoinedString
|
||||
val stderr = result.errorOutputAsJoinedString
|
||||
|
||||
@@ -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<GitRepository>()
|
||||
|
||||
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<ExpireAfterRepoStateChanged>(project)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -30,22 +30,25 @@ public final class LocalChangesWouldBeOverwrittenHelper {
|
||||
final Collection<String> absolutePaths = GitUtil.toAbsolute(root, relativeFilePaths);
|
||||
final List<Change> 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<? extends Change> changes,
|
||||
@NotNull Collection<String> absolutePaths) {
|
||||
public static void showErrorDialog(@NotNull Project project,
|
||||
@NotNull String operationName,
|
||||
@Nls String description,
|
||||
@NotNull List<? extends Change> changes,
|
||||
@NotNull Collection<String> 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();
|
||||
|
||||
@@ -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"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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`() {
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user