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
This commit is contained in:
Ilia.Shulgin
2026-03-03 11:56:54 +00:00
committed by intellij-monorepo-bot
parent 5e3abfb477
commit 08209238ef
8 changed files with 44 additions and 237 deletions
-4
View File
@@ -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
@@ -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}''
@@ -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))
@@ -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)
}
}
@@ -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"
}
}
@@ -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<GitFetchSpec> {
return getAllFailedResults()
.filter { GitFetchErrorFormatter.isCheckedOutBranchError(it.error) }
.map { it.fetchSpec }
}
private fun getAllFailedResults(): List<FailedSingleRemoteResult> {
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)
}
}
@@ -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<GitRepository>,
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<GitRepository>,
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<GitRepository>, localBranchNames: List<String>): 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<GitRepository, GitBranchPair>()
// Otherwise, perform fetch using remote:local refspec
val fetchTargets = mutableListOf<GitFetchSpec>()
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<GitReposi
}
}
@VisibleForTesting
internal fun prepareUpdateTargets(
repositories: Collection<GitRepository>, localBranchNames: List<String>,
): 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<GitRepository, GitBranchPair>()
// Otherwise, perform fetch using remote:local refspec
val fetchTargets = mutableListOf<GitFetchSpec>()
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<GitFetchSpec>,
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<String>, repositories: Collection<GitRepository>) =
repositories
.flatMap(GitRepository::getBranchTrackInfos)
@@ -133,19 +101,15 @@ internal fun hasRemotes(project: Project): Boolean {
internal fun hasAnyRemotes(repositories: Collection<GitRepository>): Boolean = repositories.any { it.remotes.isNotEmpty() }
internal fun hasTrackingConflicts(
conflictingLocalBranches: Map<GitRepository, GitLocalBranch>,
remoteBranchName: String,
): Boolean =
internal fun hasTrackingConflicts(conflictingLocalBranches: Map<GitRepository, GitLocalBranch>,
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<GitFetchSpec>,
val updateProcessTargets: Map<GitRepository, GitBranchPair>,
)
@@ -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)
}
}