IDEA-320968 git: do not use .sh scripts for callbacks on Windows if custom ssh executable is configured

GitOrigin-RevId: c42638ba3a1516560611dcbf75032d054620d0c0
This commit is contained in:
Aleksey Pivovarov
2023-09-26 15:58:49 +02:00
committed by intellij-monorepo-bot
parent 7a5f784b55
commit aa70e4b6f4
10 changed files with 152 additions and 23 deletions

View File

@@ -118,4 +118,8 @@ public interface Repository extends Disposable {
@NonNls
@NotNull
String toLogString();
default boolean isDisposed() {
return false;
}
}

View File

@@ -26,6 +26,8 @@ public abstract class RepositoryImpl implements Repository {
@NotNull private final Project myProject;
@NotNull private final VirtualFile myRootDir;
private boolean myDisposed;
protected RepositoryImpl(@NotNull Project project,
@NotNull VirtualFile dir,
@NotNull Disposable parentDisposable) {
@@ -62,8 +64,14 @@ public abstract class RepositoryImpl implements Repository {
return getCurrentRevision() == null;
}
@Override
public boolean isDisposed() {
return myDisposed;
}
@Override
public void dispose() {
myDisposed = true;
}
@Override

View File

@@ -561,7 +561,7 @@
description="Use workaround for Native SSH support in Git. Batch file cannot properly handle arguments with newlines when used as SSH_ASKPASS."/>
<registryKey key="git.use.setsid.for.native.ssh" defaultValue="true"
description="Wrap 'git' process with 'setsid' for remote operations.
This fixes issue with SSH_ASKPASS if IDE is executed from terminal or under DM/WM that pass system TTY from Xorg to GUI applications."/>
This fixes issue with SSH_ASKPASS if IDE is executed from terminal or under DM/WM that pass system TTY from Xorg to GUI applications. IDEA-201054"/>
<registryKey key="git.use.setsid.wait.for.wsl.ssh" defaultValue="true"
description="Wrap 'git' process with 'setsid -w' for remote operations with WSL executable."/>
<registryKey key="git.wsl.exe.executable.no.shell" defaultValue="false"

View File

@@ -76,6 +76,8 @@ public final class GitCommand {
public static final @NonNls String GIT_SSH_ASK_PASS_ENV = "SSH_ASKPASS";
public static final @NonNls String SSH_ASKPASS_REQUIRE_ENV = "SSH_ASKPASS_REQUIRE";
public static final @NonNls String DISPLAY_ENV = "DISPLAY";
public static final @NonNls String GIT_SSH_ENV = "GIT_SSH";
public static final @NonNls String GIT_SSH_COMMAND_ENV = "GIT_SSH_COMMAND";
/**
* Marker-ENV, that lets git hooks to detect us if needed.
*/

View File

@@ -57,6 +57,7 @@ public abstract class GitHandler {
private final GitCommand myCommand;
private boolean myPreValidateExecutable = true;
private boolean myEnableInteractiveCallbacks = true;
protected final GeneralCommandLine myCommandLine;
private final Map<String, String> myCustomEnv = new HashMap<>();
@@ -397,6 +398,20 @@ public abstract class GitHandler {
return myPreValidateExecutable;
}
/**
* See {@link GitImplBase#run(Computable, Computable)}
*/
public boolean isEnableInteractiveCallbacks() {
return myEnableInteractiveCallbacks;
}
/**
* See {@link GitImplBase#run(Computable, Computable)}
*/
public void setEnableInteractiveCallbacks(boolean enableInteractiveCallbacks) {
myEnableInteractiveCallbacks = enableInteractiveCallbacks;
}
void runInCurrentThread() throws IOException {
try {
start();

View File

@@ -12,13 +12,16 @@ import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.EnvironmentUtil;
import externalApp.nativessh.NativeSshAskPassAppHandler;
import git4idea.GitUtil;
import git4idea.config.GitExecutable;
import git4idea.config.GitVcsApplicationSettings;
import git4idea.config.GitVersion;
import git4idea.config.GitVersionSpecialty;
import git4idea.config.*;
import git4idea.http.GitAskPassAppHandler;
import git4idea.repo.GitProjectConfigurationCache;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -177,12 +180,53 @@ public final class GitHandlerAuthenticationManager implements AutoCloseable {
private void addHandlerPathToEnvironment(@NotNull String env,
@NotNull ExternalProcessHandlerService<?> service) throws IOException {
GitExecutable executable = myHandler.getExecutable();
boolean useBatchFile = SystemInfo.isWindows &&
executable.isLocal() &&
(!Registry.is("git.use.shell.script.on.windows") ||
!GitVersionSpecialty.CAN_USE_SHELL_HELPER_SCRIPT_ON_WINDOWS.existsIn(myVersion));
File scriptFile = service.getCallbackScriptPath(executable.getId(), new GitScriptGenerator(executable), useBatchFile);
File scriptFile = service.getCallbackScriptPath(executable.getId(),
new GitScriptGenerator(executable),
shouldUseBatchScript(executable));
String scriptPath = executable.convertFilePath(scriptFile);
myHandler.addCustomEnvironmentVariable(env, scriptPath);
}
private boolean shouldUseBatchScript(@NotNull GitExecutable executable) {
if (!SystemInfo.isWindows) return false;
if (!executable.isLocal()) return false;
if (Registry.is("git.use.shell.script.on.windows") &&
GitVersionSpecialty.CAN_USE_SHELL_HELPER_SCRIPT_ON_WINDOWS.existsIn(myVersion)) {
return isCustomSshExecutableConfigured();
}
return true;
}
private boolean isCustomSshExecutableConfigured() {
String sshCommand = readSshCommand();
String command = StringUtil.trim(StringUtil.unquoteString(StringUtil.notNullize(sshCommand)));
// do not treat 'ssh -vvv' as custom executable
return !command.isEmpty() && !command.startsWith("ssh ");
}
@Nullable
private String readSshCommand() {
String sshCommand = EnvironmentUtil.getValue(GitCommand.GIT_SSH_COMMAND_ENV);
if (sshCommand != null) return sshCommand;
sshCommand = EnvironmentUtil.getValue(GitCommand.GIT_SSH_ENV);
if (sshCommand != null) return sshCommand;
VirtualFile root = myHandler.getExecutableContext().getRoot();
if (root == null) return null;
GitRepository repo = GitRepositoryManager.getInstance(myProject).getRepositoryForRoot(root);
if (repo != null) {
return GitProjectConfigurationCache.getInstance(myProject).readRepositoryConfig(repo, GitConfigUtil.CORE_SSH_COMMAND);
}
else {
try {
return GitConfigUtil.getValue(myProject, root, GitConfigUtil.CORE_SSH_COMMAND);
}
catch (VcsException e) {
LOG.warn(e);
return null;
}
}
}
}

View File

@@ -154,7 +154,7 @@ public abstract class GitImplBase implements Git {
throw new ProcessCanceledException();
}
if (project != null) {
if (project != null && handler.isEnableInteractiveCallbacks()) {
try (GitHandlerAuthenticationManager authenticationManager = GitHandlerAuthenticationManager.prepare(project, handler, version)) {
try (GitHandlerRebaseEditorManager ignored = prepareGeneralPurposeEditor(project, handler)) {
GitCommandResult result = doRun(handler, version, outputCollector);

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2020 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.config;
import com.intellij.openapi.project.Project;
@@ -28,6 +28,7 @@ public final class GitConfigUtil {
public static final @NlsSafe String USER_EMAIL = "user.email";
public static final @NlsSafe String CORE_AUTOCRLF = "core.autocrlf";
public static final @NlsSafe String CREDENTIAL_HELPER = "credential.helper";
public static final @NlsSafe String CORE_SSH_COMMAND = "core.sshCommand";
public static final @NlsSafe String LOG_OUTPUT_ENCODING = "i18n.logoutputencoding";
public static final @NlsSafe String COMMIT_ENCODING = "i18n.commitencoding";
public static final @NlsSafe String COMMIT_TEMPLATE = "commit.template";
@@ -43,6 +44,7 @@ public final class GitConfigUtil {
@Nullable @NonNls String keyMask,
@NotNull Map<String, String> result) throws VcsException {
GitLineHandler h = new GitLineHandler(project, root, GitCommand.CONFIG);
h.setEnableInteractiveCallbacks(false);
h.setSilent(true);
h.addParameters("--null");
if (keyMask != null) {
@@ -66,12 +68,15 @@ public final class GitConfigUtil {
}
public static @Nullable String getValue(@NotNull Project project, @NotNull VirtualFile root, @NotNull @NonNls String key) throws VcsException {
@Nullable
public static String getValue(@NotNull Project project, @NotNull VirtualFile root, @NotNull @NonNls String key) throws VcsException {
GitLineHandler h = new GitLineHandler(project, root, GitCommand.CONFIG);
return getValue(h, key);
}
private static @Nullable String getValue(@NotNull GitLineHandler h, @NotNull @NonNls String key) throws VcsException {
@Nullable
private static String getValue(@NotNull GitLineHandler h, @NotNull @NonNls String key) throws VcsException {
h.setEnableInteractiveCallbacks(false);
h.setSilent(true);
h.addParameters("--null", "--get", key);
GitCommandResult result = Git.getInstance().runCommand(h);
@@ -87,7 +92,8 @@ public final class GitConfigUtil {
* Converts the git config boolean value (which can be specified in various ways) to Java Boolean.
* @return true if the value represents "true", false if the value represents "false", null if the value doesn't look like a boolean value.
*/
public static @Nullable Boolean getBooleanValue(@Nullable @NonNls String value) {
@Nullable
public static Boolean getBooleanValue(@Nullable @NonNls String value) {
if (value == null) return null;
value = StringUtil.toLowerCase(value);
if (ContainerUtil.newHashSet("true", "yes", "on", "1").contains(value)) return true;
@@ -98,7 +104,8 @@ public final class GitConfigUtil {
/**
* Get commit encoding for the specified root, or UTF-8 if the encoding is note explicitly specified
*/
public static @NotNull String getCommitEncoding(@NotNull Project project, @NotNull VirtualFile root) {
@NotNull
public static String getCommitEncoding(@NotNull Project project, @NotNull VirtualFile root) {
String encoding = null;
try {
encoding = getValue(project, root, COMMIT_ENCODING);

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.vcs.VcsLocaleHelper
import git4idea.commands.GitHandler
import git4idea.i18n.GitBundle
import git4idea.repo.GitConfigKey
import git4idea.repo.GitConfigurationCache
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
@@ -195,7 +196,7 @@ sealed class GitExecutable {
private data class WslSupportedLocaleKey(
val distribution: WSLDistribution
) : GitConfigurationCache.ConfigKey<Map<String, String>?>
) : GitConfigKey<Map<String, String>?>
private fun computeWslSupportedLocaleKey(distribution: WSLDistribution): Map<String, String>? {
val knownLocales = listOf(VcsLocaleHelper.EN_UTF_LOCALE, VcsLocaleHelper.C_UTF_LOCALE)

View File

@@ -2,14 +2,20 @@
package git4idea.repo
import com.intellij.concurrency.ConcurrentCollectionFactory
import com.intellij.dvcs.repo.VcsRepositoryManager
import com.intellij.dvcs.repo.VcsRepositoryMappingListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.VcsException
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.messages.MessageBusConnection
import git4idea.config.GitConfigUtil
import git4idea.config.GitExecutableListener
import git4idea.config.GitExecutableManager
import org.jetbrains.annotations.ApiStatus
@@ -19,21 +25,60 @@ import java.util.concurrent.ExecutionException
@ApiStatus.Experimental
@Service(Service.Level.APP)
class GitConfigurationCache : Disposable {
class GitConfigurationCache : GitConfigurationCacheBase() {
companion object {
@JvmStatic
fun getInstance(): GitConfigurationCache = service()
}
}
private val cache: MutableMap<ConfigKey<*>, CompletableFuture<*>> = ConcurrentCollectionFactory.createConcurrentMap()
@ApiStatus.Experimental
@Service(Service.Level.PROJECT)
class GitProjectConfigurationCache(val project: Project) : GitConfigurationCacheBase() {
companion object {
@JvmStatic
fun getInstance(project: Project): GitProjectConfigurationCache = project.service()
}
init {
val connection: MessageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(this)
val connection: MessageBusConnection = project.messageBus.connect(this)
connection.subscribe<VcsRepositoryMappingListener>(VcsRepositoryManager.VCS_REPOSITORY_MAPPING_UPDATED, VcsRepositoryMappingListener {
clearInvalidKeys()
})
}
@RequiresBackgroundThread
fun <T> readRepositoryConfig(repository: GitRepository, key: String): String? {
return computeCachedValue(RepoConfigKey(repository, key)) {
try {
GitConfigUtil.getValue(repository.getProject(), repository.getRoot(), key)
}
catch (e: VcsException) {
logger<GitProjectConfigurationCache>().warn(e)
null
}
}
}
private fun clearInvalidKeys() {
cache.keys.removeIf {
it is GitRepositoryConfigKey && it.repository.isDisposed()
}
}
data class RepoConfigKey(override val repository: GitRepository, val key: String) : GitRepositoryConfigKey<String?>
}
abstract class GitConfigurationCacheBase : Disposable {
protected val cache: MutableMap<GitConfigKey<*>, CompletableFuture<*>> = ConcurrentCollectionFactory.createConcurrentMap()
init {
val connection: MessageBusConnection = ApplicationManager.getApplication().messageBus.connect(this)
connection.subscribe<GitExecutableListener>(GitExecutableManager.TOPIC, GitExecutableListener { clearCache() })
}
@RequiresBackgroundThread
fun <T> computeCachedValue(configKey: ConfigKey<T>, computeValue: () -> T): T {
fun <T> computeCachedValue(configKey: GitConfigKey<T>, computeValue: () -> T): T {
val future = CompletableFuture<T>()
@Suppress("UNCHECKED_CAST")
@@ -79,6 +124,9 @@ class GitConfigurationCache : Disposable {
override fun dispose() {
}
interface ConfigKey<T>
}
interface GitConfigKey<T>
interface GitRepositoryConfigKey<T> : GitConfigKey<T> {
val repository: GitRepository
}