From 08209238ef31405a44ec587e8ba5de2a1300cefa Mon Sep 17 00:00:00 2001 From: "Ilia.Shulgin" Date: Fri, 20 Feb 2026 09:40:10 +0100 Subject: [PATCH] Revert "[git] IJPL-207004 Add a notification action to retry fetch of checked out branch with --update-head-ok flag" This reverts commit c733994e. Action will be just disabled in case if it's checked out in any working tree. (cherry picked from commit 270e543268d8fd8efd47c23905c9003e44a8e9e8) IJ-CR-194094 GitOrigin-RevId: abafa08103080fadb6ec496805073229a72e0722 --- plugins/git4idea/api-dump-unreviewed.txt | 4 - .../resources/messages/GitBundle.properties | 2 - .../GitAddCommitToRemoteBranchOperation.kt | 1 - .../git4idea/fetch/GitFetchErrorFormatter.kt | 33 ----- .../src/git4idea/fetch/GitFetchSpec.kt | 3 - .../src/git4idea/fetch/GitFetchSupportImpl.kt | 44 +----- .../ui/branch/GitBranchActionsUtil.kt | 128 ++++++------------ ...GitFetchBranchesCheckedOutElsewhereTest.kt | 66 --------- 8 files changed, 44 insertions(+), 237 deletions(-) delete mode 100644 plugins/git4idea/src/git4idea/fetch/GitFetchErrorFormatter.kt delete mode 100644 plugins/git4idea/tests/git4idea/ui/branch/GitFetchBranchesCheckedOutElsewhereTest.kt diff --git a/plugins/git4idea/api-dump-unreviewed.txt b/plugins/git4idea/api-dump-unreviewed.txt index 6df83eacf281..04c7485a1192 100644 --- a/plugins/git4idea/api-dump-unreviewed.txt +++ b/plugins/git4idea/api-dump-unreviewed.txt @@ -2217,10 +2217,6 @@ f:git4idea.diff.GitSubmoduleDiffRequestProvider - canCreate(com.intellij.openapi.project.Project,com.intellij.openapi.vcs.changes.Change):Z - isEquals(com.intellij.openapi.vcs.changes.Change,com.intellij.openapi.vcs.changes.Change):com.intellij.util.ThreeState - process(com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer,com.intellij.openapi.util.UserDataHolder,com.intellij.openapi.progress.ProgressIndicator):com.intellij.diff.requests.DiffRequest -f:git4idea.fetch.GitFetchErrorFormatter -- sf:INSTANCE:git4idea.fetch.GitFetchErrorFormatter -- f:format(java.lang.String):java.lang.String -- f:isCheckedOutBranchError(java.lang.String):Z git4idea.fetch.GitFetchHandler - sf:Companion:git4idea.fetch.GitFetchHandler$Companion - sf:EP_NAME:com.intellij.openapi.extensions.ExtensionPointName diff --git a/plugins/git4idea/shared/resources/messages/GitBundle.properties b/plugins/git4idea/shared/resources/messages/GitBundle.properties index 3190d541afc8..2781c1475aca 100644 --- a/plugins/git4idea/shared/resources/messages/GitBundle.properties +++ b/plugins/git4idea/shared/resources/messages/GitBundle.properties @@ -1074,8 +1074,6 @@ action.Git.Stage.Disable.description=Disable staging area and switch to changeli branches.fetch.finished=Fetched {0} targets branches.update.failed=Update failed branches.updating.process=Updating branches\u2026 -branches.update.anyway.notification.action=Update anyway -branches.update.error.branch.checked.out=Branch ''{0}'' is checked out at ''{1}'' branches.checking.existing.commits.process=Checking existing commits\u2026 branches.create.new.branch.dialog.title=Create New Branch branches.tag.0=Tag ''{0}'' diff --git a/plugins/git4idea/src/git4idea/actions/addCommit/GitAddCommitToRemoteBranchOperation.kt b/plugins/git4idea/src/git4idea/actions/addCommit/GitAddCommitToRemoteBranchOperation.kt index b70bcc4f5f24..a4768b20865d 100644 --- a/plugins/git4idea/src/git4idea/actions/addCommit/GitAddCommitToRemoteBranchOperation.kt +++ b/plugins/git4idea/src/git4idea/actions/addCommit/GitAddCommitToRemoteBranchOperation.kt @@ -84,7 +84,6 @@ internal class GitAddCommitToRemoteBranchOperation( remote = remote, refspec = "refs/heads/$branchName:refs/remotes/${remote.name}/$branchName", unshallow = false, - updateHeadOk = false, ) val result = GitFetchSupport.fetchSupport(project).fetch(listOf(fetchSpec)) diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchErrorFormatter.kt b/plugins/git4idea/src/git4idea/fetch/GitFetchErrorFormatter.kt deleted file mode 100644 index 44188a672f8a..000000000000 --- a/plugins/git4idea/src/git4idea/fetch/GitFetchErrorFormatter.kt +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package git4idea.fetch - -import com.intellij.openapi.ui.getPresentablePath -import git4idea.branch.GitBranchUtil -import git4idea.i18n.GitBundle -import org.jetbrains.annotations.Nls -import java.util.regex.Pattern - -object GitFetchErrorFormatter { - private val BRANCH_CHECKED_OUT_PATTERN = Pattern.compile("refusing to fetch into branch '(.*)' checked out at '(.*)'") - - fun isCheckedOutBranchError(errorMessage: @Nls String): Boolean { - return BRANCH_CHECKED_OUT_PATTERN.matcher(errorMessage).matches() - } - - fun format(errorMessage: @Nls String): @Nls String { - return tryFormatCheckedOutBranchError(errorMessage) ?: errorMessage - } - - private fun tryFormatCheckedOutBranchError(errorMessage: @Nls String): @Nls String? { - val matcher = BRANCH_CHECKED_OUT_PATTERN.matcher(errorMessage) - if (!matcher.matches()) return null - - val branchRef = matcher.group(1) - val path = matcher.group(2) - - val shortBranchName = GitBranchUtil.stripRefsPrefix(branchRef) - val shortPath = getPresentablePath(path) - - return GitBundle.message("branches.update.error.branch.checked.out", shortBranchName, shortPath) - } -} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSpec.kt b/plugins/git4idea/src/git4idea/fetch/GitFetchSpec.kt index b3e098bdef84..e6f7f0021904 100644 --- a/plugins/git4idea/src/git4idea/fetch/GitFetchSpec.kt +++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSpec.kt @@ -13,7 +13,6 @@ data class GitFetchSpec( val remote: GitRemote, val refspec: String? = null, val unshallow: Boolean = false, - val updateHeadOk: Boolean = false, // allow fetch to update the head which corresponds to the current branch val fetchTagsMode: GitFetchTagsMode = GitVcsSettings.getInstance(repository.project).fetchTagsMode, val authMode: AuthenticationMode? = null, ) { @@ -26,13 +25,11 @@ data class GitFetchSpec( add(NO_RECURSE_SUBMODULES) fetchTagsMode.param?.let { add(it) } if (unshallow) add(UNSHALLOW) - if (updateHeadOk) add(UPDATE_HEAD_OK) }.toTypedArray() } companion object { private const val NO_RECURSE_SUBMODULES = "--recurse-submodules=no" private const val UNSHALLOW = "--unshallow" - private const val UPDATE_HEAD_OK = "--update-head-ok" } } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt index 6ceb2ce2cb6b..c049233b4f99 100644 --- a/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt +++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt @@ -6,7 +6,6 @@ import com.intellij.dvcs.MultiRootMessage import com.intellij.execution.process.ProcessOutputTypes import com.intellij.externalProcessAuthHelper.AuthenticationGate import com.intellij.externalProcessAuthHelper.RestrictingAuthenticationGate -import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationType import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger @@ -24,9 +23,7 @@ import com.intellij.openapi.util.text.HtmlChunk import com.intellij.openapi.vcs.VcsException import com.intellij.openapi.vcs.VcsNotifier import com.intellij.openapi.vcs.changes.actions.VcsStatisticsCollector -import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.util.concurrency.AppExecutorUtil -import git4idea.GitDisposable import git4idea.GitNotificationIdsHolder import git4idea.GitUtil.findRemoteByName import git4idea.GitUtil.mention @@ -38,8 +35,6 @@ import git4idea.config.GitConfigUtil import git4idea.i18n.GitBundle import git4idea.repo.GitRemote import git4idea.repo.GitRepository -import git4idea.ui.branch.fetchBranches -import kotlinx.coroutines.launch import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls import java.util.concurrent.CancellationException @@ -285,10 +280,7 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo fun error(): @Nls String { val errorMessage = multiRemoteMessage(true) for ((remote, result) in results) { - if (result.error != null) { - val presentableErrorMessage = GitFetchErrorFormatter.format(result.error) - errorMessage.append(remote, presentableErrorMessage) - } + if (result.error != null) errorMessage.append(remote, result.error) } return errorMessage.asString() } @@ -379,20 +371,6 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo val notification = VcsNotifier.standardNotification() .createNotification(title, message, NotificationType.ERROR) notification.setDisplayId(GitNotificationIdsHolder.FETCH_RESULT_ERROR) - - if (allFailuresAreCheckedOutBranches()) { - notification.addAction(NotificationAction.createSimpleExpiring( - GitBundle.message("branches.update.anyway.notification.action")) { - notification.expire() - GitDisposable.getInstance(project).coroutineScope.launch { - withBackgroundProgress(project, GitBundle.message("branches.updating.process")) { - val failedDueToCheckedOutBranch = findFailedDueToCheckedOutBranch() - fetchBranches(project, failedDueToCheckedOutBranch, updateHeadOk = true) - } - } - }) - } - vcsNotifier.notify(notification) } @@ -427,25 +405,5 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo sb.appendRaw(text) } } - - private fun allFailuresAreCheckedOutBranches(): Boolean { - return getAllFailedResults().all { GitFetchErrorFormatter.isCheckedOutBranchError(it.error) } - } - - private fun findFailedDueToCheckedOutBranch(): List { - return getAllFailedResults() - .filter { GitFetchErrorFormatter.isCheckedOutBranchError(it.error) } - .map { it.fetchSpec } - } - - private fun getAllFailedResults(): List { - return results.values.flatMap { repoResult -> - repoResult.results.values.mapNotNull { singleRemoteResult -> - singleRemoteResult.error?.let { error -> FailedSingleRemoteResult(singleRemoteResult.fetchSpec, error) } - } - } - } - - private data class FailedSingleRemoteResult(val fetchSpec: GitFetchSpec, val error: @Nls String) } } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitBranchActionsUtil.kt b/plugins/git4idea/src/git4idea/ui/branch/GitBranchActionsUtil.kt index ca3e14b0f81c..da24262f7005 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitBranchActionsUtil.kt +++ b/plugins/git4idea/src/git4idea/ui/branch/GitBranchActionsUtil.kt @@ -19,41 +19,64 @@ import git4idea.branch.GitBranchPair import git4idea.branch.GitBranchUtil import git4idea.branch.GitNewBranchDialog import git4idea.config.GitVcsSettings -import git4idea.fetch.GitFetchResult import git4idea.fetch.GitFetchSpec import git4idea.fetch.GitFetchSupport import git4idea.i18n.GitBundle import git4idea.repo.GitRepository import git4idea.update.GitUpdateExecutionProcess -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jetbrains.annotations.Nls -import org.jetbrains.annotations.VisibleForTesting import javax.swing.Icon @JvmOverloads -internal fun createOrCheckoutNewBranch( - project: Project, - repositories: Collection, - startPoint: String, - @Nls(capitalization = Nls.Capitalization.Title) - title: String = GitBundle.message("branches.create.new.branch.dialog.title"), - initialName: String? = null, -) { - val options = GitNewBranchDialog(project, repositories, title, initialName, showResetOption = true, localConflictsAllowed = true).showAndGetOptions() - ?: return +internal fun createOrCheckoutNewBranch(project: Project, + repositories: Collection, + startPoint: String, + @Nls(capitalization = Nls.Capitalization.Title) + title: String = GitBundle.message("branches.create.new.branch.dialog.title"), + initialName: String? = null) { + val options = GitNewBranchDialog(project, repositories, title, initialName, showResetOption = true, localConflictsAllowed = true).showAndGetOptions() ?: return GitBranchCheckoutOperation(project, options.repositories).perform(startPoint, options) } internal fun updateBranches(project: Project, repositories: Collection, localBranchNames: List): Job { + val repoToTrackingInfos = + repositories.associateWith { it.branchTrackInfos.filter { info -> localBranchNames.contains(info.localBranch.name) } } + if (repoToTrackingInfos.isEmpty()) return CompletableDeferred(Unit) + return GitDisposable.getInstance(project).coroutineScope.launch { withBackgroundProgress(project, GitBundle.message("branches.updating.process")) { - val (fetchTargets, updateProcessTargets) = prepareUpdateTargets(repositories, localBranchNames) + // If a branch is checked out, do update via GitUpdateExecutionProcess + val updateProcessTargets = HashMap() + // Otherwise, perform fetch using remote:local refspec + val fetchTargets = mutableListOf() + + for ((repo, trackingInfos) in repoToTrackingInfos) { + val currentBranch = repo.currentBranch + for (trackingInfo in trackingInfos) { + val localBranch = trackingInfo.localBranch + val remoteBranch = trackingInfo.remoteBranch + if (localBranch == currentBranch) { + updateProcessTargets[repo] = GitBranchPair(currentBranch, remoteBranch) + } + else { + // Fast-forward all non-current branches in the selection + val localBranchName = localBranch.name + val remoteBranchName = remoteBranch.nameForRemoteOperations + fetchTargets.add(GitFetchSpec(repo, trackingInfo.remote, "$remoteBranchName:$localBranchName")) + } + } + } if (fetchTargets.isNotEmpty()) { - fetchBranches(project, fetchTargets, updateHeadOk = false) + val fetchSuccessful = coroutineToIndicator { + GitFetchSupport.fetchSupport(project).fetch(fetchTargets).showNotificationIfFailed(GitBundle.message("branches.update.failed")) + } + if (fetchSuccessful) { + VcsNotifier.getInstance(project).notifySuccess(BRANCHES_UPDATE_SUCCESSFUL, "", GitBundle.message("branches.fetch.finished", fetchTargets.size)) + } } if (updateProcessTargets.isNotEmpty()) { @@ -67,61 +90,6 @@ internal fun updateBranches(project: Project, repositories: Collection, localBranchNames: List, -): UpdateTargets { - val repoToTrackingInfos = - repositories.associateWith { it.branchTrackInfos.filter { info -> localBranchNames.contains(info.localBranch.name) } } - - // If a branch is checked out, do update via GitUpdateExecutionProcess - val updateProcessTargets = HashMap() - // Otherwise, perform fetch using remote:local refspec - val fetchTargets = mutableListOf() - - for ((repo, trackingInfos) in repoToTrackingInfos) { - val currentBranch = repo.currentBranch - for (trackingInfo in trackingInfos) { - val localBranch = trackingInfo.localBranch - val remoteBranch = trackingInfo.remoteBranch - if (localBranch == currentBranch) { - updateProcessTargets[repo] = GitBranchPair(currentBranch, remoteBranch) - } - else { - // Fast-forward all non-current branches in the selection - val localBranchName = localBranch.name - val remoteBranchName = remoteBranch.nameForRemoteOperations - fetchTargets.add(GitFetchSpec(repo, trackingInfo.remote, "$remoteBranchName:$localBranchName")) - } - } - } - - return UpdateTargets(fetchTargets, updateProcessTargets) -} - -internal suspend fun fetchBranches( - project: Project, - fetchTargets: List, - updateHeadOk: Boolean, -): GitFetchResult { - return withContext(Dispatchers.Default) { - val specsToFetch = if (updateHeadOk) { - fetchTargets.map { it.copy(updateHeadOk = true) } - } - else { - fetchTargets - } - val result = coroutineToIndicator { - GitFetchSupport.fetchSupport(project).fetch(specsToFetch) - } - val fetchSuccessful = result.showNotificationIfFailed(GitBundle.message("branches.update.failed")) - if (fetchSuccessful) { - VcsNotifier.getInstance(project).notifySuccess(BRANCHES_UPDATE_SUCCESSFUL, "", GitBundle.message("branches.fetch.finished", specsToFetch.size)) - } - result - } -} - internal fun isTrackingInfosExist(branchNames: List, repositories: Collection) = repositories .flatMap(GitRepository::getBranchTrackInfos) @@ -133,19 +101,15 @@ internal fun hasRemotes(project: Project): Boolean { internal fun hasAnyRemotes(repositories: Collection): Boolean = repositories.any { it.remotes.isNotEmpty() } -internal fun hasTrackingConflicts( - conflictingLocalBranches: Map, - remoteBranchName: String, -): Boolean = +internal fun hasTrackingConflicts(conflictingLocalBranches: Map, + remoteBranchName: String): Boolean = conflictingLocalBranches.any { (repo, branch) -> val trackInfo = GitBranchUtil.getTrackInfoForBranch(repo, branch) trackInfo != null && !GitReference.BRANCH_NAME_HASHING_STRATEGY.equals(remoteBranchName, trackInfo.remoteBranch.name) } -internal abstract class BranchGroupingAction( - private val key: GroupingKey, - icon: Icon? = null, -) : ToggleAction(key.text, key.description, icon), DumbAware { +internal abstract class BranchGroupingAction(private val key: GroupingKey, + icon: Icon? = null) : ToggleAction(key.text, key.description, icon), DumbAware { override fun getActionUpdateThread(): ActionUpdateThread { return ActionUpdateThread.EDT } @@ -160,9 +124,3 @@ internal abstract class BranchGroupingAction( GitVcsSettings.getInstance(project).setBranchGroupingSettings(key, state) } } - -@VisibleForTesting -internal data class UpdateTargets( - val fetchTargets: List, - val updateProcessTargets: Map, -) \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/ui/branch/GitFetchBranchesCheckedOutElsewhereTest.kt b/plugins/git4idea/tests/git4idea/ui/branch/GitFetchBranchesCheckedOutElsewhereTest.kt deleted file mode 100644 index 57cb2e53a80f..000000000000 --- a/plugins/git4idea/tests/git4idea/ui/branch/GitFetchBranchesCheckedOutElsewhereTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package git4idea.ui.branch - -import com.intellij.openapi.ui.getPresentablePath -import com.intellij.openapi.vcs.Executor.cd -import git4idea.i18n.GitBundle -import git4idea.repo.GitWorkTreeBaseTest -import git4idea.test.assertCurrentRevision -import git4idea.test.checkout -import git4idea.test.git -import git4idea.test.initRepo -import git4idea.test.tac -import kotlinx.coroutines.runBlocking -import java.nio.file.Files -import java.nio.file.Path - -class GitFetchBranchesCheckedOutElsewhereTest : GitWorkTreeBaseTest() { - lateinit var bro: Path - lateinit var parent: Path - - override fun initMainRepo(): Path { - val mainDir = testNioRoot.resolve("main") - Files.createDirectories(mainDir) - initRepo(project, mainDir, true) - return mainDir - } - - override fun setUp() { - super.setUp() - parent = prepareRemoteRepo(myRepo) - bro = createBroRepo("bro", parent) - } - - fun `test fetch blocked by checked out branch then retries with update-head-ok flag`() { - cd(myMainRoot) - git("checkout -b feature") - tac("a.txt", "main") - git("push -u origin feature") - - cd(bro) - git("fetch origin feature") - git("checkout feature") - val broCommit = tac("b.txt", "bro") - git("push -u origin feature") - - myRepo.update() - - val fetchTargets = prepareUpdateTargets(listOf(myRepo), listOf("feature")).fetchTargets - runBlocking { fetchBranches(project, fetchTargets, updateHeadOk = false) } - - val title = GitBundle.message("branches.update.failed") - val message = GitBundle.message("branches.update.error.branch.checked.out", "feature", getPresentablePath(myMainRoot.toString())) - assertErrorNotification(title, message, actions = listOf( - GitBundle.message("branches.update.anyway.notification.action") - )) - - runBlocking { fetchBranches(project, fetchTargets, updateHeadOk = true) } - - assertSuccessfulNotification(GitBundle.message("branches.fetch.finished", 1)) - - cd(myMainRoot) - checkout("-b another") - myRepo.checkout("feature") - myRepo.assertCurrentRevision(broCommit) - } -}