[git] IJPL-73913 Suggest stashing/shelving changes and retry cherry-pick

GitOrigin-RevId: d73345928d536ddfe9f22666b4f2dd3272263c31
This commit is contained in:
Ilia.Shulgin
2024-10-01 09:03:41 +02:00
committed by intellij-monorepo-bot
parent ed0d05e826
commit 37600c5ef0
15 changed files with 404 additions and 102 deletions

View File

@@ -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() {

View File

@@ -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}

View File

@@ -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
}
}
}

View File

@@ -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"

View File

@@ -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()
}
}

View File

@@ -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()
}
}
})
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)
}
})
}
}

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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"))
)
}
}

View File

@@ -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`() {

View File

@@ -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}"