From 57933ab7d42666435f54d26bc8bf9d8e4f3625ee Mon Sep 17 00:00:00 2001 From: "natalia.ponomareva" Date: Wed, 2 Sep 2026 12:47:09 +0200 Subject: [PATCH] [git] Fixed caching git-trusted failure (IJPL-254472) (cherry picked from commit a11ab71c33694546ac840e02f6de59bf48a78d92) (cherry picked from commit 5cef50f116adffe45a73a92c0cc8ca6c6928b418) IJ-MR-222169 GitOrigin-RevId: d111f26b7b4d53b3de78db64949c8b76a6b5d9b0 --- plugins/git4idea/backend/api-dump.txt | 3 ++ .../GitCommandNotTrustedException.java | 17 +++++++ .../backend/src/commands/GitHandler.java | 6 +-- .../src/config/GitExecutableFileTester.java | 17 ++++++- .../GitExecutableManagerTrustCacheTest.kt | 48 +++++++++++++++++++ 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 plugins/git4idea/backend/src/commands/GitCommandNotTrustedException.java create mode 100644 plugins/git4idea/tests/git4idea/config/GitExecutableManagerTrustCacheTest.kt diff --git a/plugins/git4idea/backend/api-dump.txt b/plugins/git4idea/backend/api-dump.txt index 098fffbb9a24..293f4e5ed348 100644 --- a/plugins/git4idea/backend/api-dump.txt +++ b/plugins/git4idea/backend/api-dump.txt @@ -97,6 +97,9 @@ git4idea.commands.Git - a:runCommand(com.intellij.openapi.util.Computable):git4idea.commands.GitCommandResult - a:runCommand(git4idea.commands.GitLineHandler):git4idea.commands.GitCommandResult - a:runCommandWithoutCollectingOutput(git4idea.commands.GitLineHandler):git4idea.commands.GitCommandResult +f:git4idea.commands.GitCommandNotTrustedException +- java.lang.IllegalStateException +- (java.lang.String):V c:git4idea.commands.GitCommandResult - s:error(java.lang.String):git4idea.commands.GitCommandResult - getOutputOrThrow(I[]):java.lang.String diff --git a/plugins/git4idea/backend/src/commands/GitCommandNotTrustedException.java b/plugins/git4idea/backend/src/commands/GitCommandNotTrustedException.java new file mode 100644 index 000000000000..52bdc3bb0a79 --- /dev/null +++ b/plugins/git4idea/backend/src/commands/GitCommandNotTrustedException.java @@ -0,0 +1,17 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.commands; + +import org.jetbrains.annotations.NotNull; + +/** + * Thrown when the project is not trusted. + *

+ * This reflects the trust state of the caller, not a property of the git executable. A cache + * keyed by the executable, such as {@code git4idea.config.GitExecutableFileTester}, must not + * store this failure: the same executable can succeed for a later, trusted caller. + */ +public final class GitCommandNotTrustedException extends IllegalStateException { + public GitCommandNotTrustedException(@NotNull String message) { + super(message); + } +} diff --git a/plugins/git4idea/backend/src/commands/GitHandler.java b/plugins/git4idea/backend/src/commands/GitHandler.java index d47e3e4013d9..333aaf9a4eac 100644 --- a/plugins/git4idea/backend/src/commands/GitHandler.java +++ b/plugins/git4idea/backend/src/commands/GitHandler.java @@ -465,12 +465,12 @@ public abstract class GitHandler { private void start() throws IOException { if (myProject == null && !TrustedProjects.isProjectTrusted(Objects.requireNonNull(getWorkingDirectory()))) { - throw new IllegalStateException("Shouldn't be possible to run a Git command in potentially untrusted project. " + - "Pass Project to GitHandler constructor if applicable."); + throw new GitCommandNotTrustedException("Shouldn't be possible to run a Git command in potentially untrusted project. " + + "Pass Project to GitHandler constructor if applicable."); } if (myProject != null && !myProject.isDefault() && !TrustedProjects.isProjectTrusted(myProject)) { - throw new IllegalStateException("Shouldn't be possible to run a Git command in the safe mode"); + throw new GitCommandNotTrustedException("Shouldn't be possible to run a Git command in the safe mode"); } if (isStarted()) { diff --git a/plugins/git4idea/backend/src/config/GitExecutableFileTester.java b/plugins/git4idea/backend/src/config/GitExecutableFileTester.java index 3740b2145ec9..e27d89ee78a6 100644 --- a/plugins/git4idea/backend/src/config/GitExecutableFileTester.java +++ b/plugins/git4idea/backend/src/config/GitExecutableFileTester.java @@ -18,6 +18,7 @@ import com.intellij.platform.ide.impl.wsl.WslEelDescriptor; import com.intellij.util.concurrency.AppJavaExecutorUtil; import git4idea.commands.Git; import git4idea.commands.GitCommand; +import git4idea.commands.GitCommandNotTrustedException; import git4idea.commands.GitCommandResult; import git4idea.commands.GitLineHandler; import git4idea.i18n.GitBundle; @@ -60,13 +61,27 @@ class GitExecutableFileTester { LOG.warn(e); result = new TestResult(e, currentLastModificationDate); - myTestMap.put(executable, result); + if (!isProjectTrustFailure(e)) { + myTestMap.put(executable, result); + } } return result; }); } + /** + * A {@link GitCommandNotTrustedException} reflects the trust state of the caller, not a + * property of {@code executable}, so it must not be cached: the same executable can be + * retested for a different, trusted caller and succeed. + */ + private static boolean isProjectTrustFailure(@NotNull Throwable e) { + for (Throwable t = e; t != null; t = t.getCause()) { + if (t instanceof GitCommandNotTrustedException) return true; + } + return false; + } + private static @NotNull GitVersion testOrAbort(@Nullable Project project, @NotNull GitExecutable executable) throws Exception { int maxAttempts = 1; diff --git a/plugins/git4idea/tests/git4idea/config/GitExecutableManagerTrustCacheTest.kt b/plugins/git4idea/tests/git4idea/config/GitExecutableManagerTrustCacheTest.kt new file mode 100644 index 000000000000..eeb18c5291ad --- /dev/null +++ b/plugins/git4idea/tests/git4idea/config/GitExecutableManagerTrustCacheTest.kt @@ -0,0 +1,48 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.config + +import com.intellij.ide.trustedProjects.TrustedProjects +import com.intellij.testFramework.junit5.SystemProperty +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.fixture.projectFixture +import com.intellij.vcs.test.vcsPlatformFixture +import git4idea.commands.GitCommandNotTrustedException +import git4idea.config.GitSaveChangesPolicy +import git4idea.test.GitPlatformTestContext +import git4idea.test.gitPlatformFixture +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@TestApplication +@SystemProperty("idea.trust.headless.disabled", "false") +internal class GitExecutableManagerTrustCacheTest { + + private val contextFixture = projectFixture(openAfterCreation = true).let { projectFixture -> + projectFixture + .vcsPlatformFixture() + .gitPlatformFixture(projectFixture, defaultSaveChangesPolicy = GitSaveChangesPolicy.SHELVE, hasRemoteGitOperation = false) + } + private val context: GitPlatformTestContext get() = contextFixture.get() + + @Test + fun `an untrusted-project failure is not cached and does not poison a later trusted call`(): Unit = with(context) { + val manager = GitExecutableManager.getInstance() + val executable = manager.getExecutable(project) + + // The fixture's own setup already tested this executable and cached a success for it; + // drop that entry so the call below genuinely re-tests under the trust state set here. + manager.dropVersionCache(executable) + TrustedProjects.setProjectTrusted(project, false) + val failure = assertThrows(GitVersionIdentificationException::class.java) { + manager.identifyVersion(project, executable) + } + assertTrue(failure.cause is GitCommandNotTrustedException) + + // Without the fix, the poisoned cache entry (same GitExecutable, unchanged mtime) + // would be replayed here even though the project is now trusted. + TrustedProjects.setProjectTrusted(project, true) + val version = manager.identifyVersion(project, executable) + assertTrue(version.isSupported) + } +}