diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index ff07100eed..1070faf655 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2582,7 +2582,11 @@ private String getPathToExe(String userGitExe) { exe = userGitExe + ".exe"; } - String[] pathDirs = System.getenv("PATH").split(File.pathSeparator); + String path = getEnvVar("PATH"); + if (path == null) { + path = ""; + } + String[] pathDirs = path.split(File.pathSeparator); for (String pathDir : pathDirs) { File exeFile = new File(pathDir, exe); @@ -2603,8 +2607,13 @@ private String getPathToExe(String userGitExe) { return null; } + /* Package protected for testing */ + String getEnvVar(String envVar) { + return System.getenv(envVar); + } + private File getFileFromEnv(String envVar, String suffix) { - String envValue = System.getenv(envVar); + String envValue = getEnvVar(envVar); if (envValue == null) { return null; } @@ -2689,6 +2698,15 @@ public File getSSHExecutable() { } } + // Check the system PATH last, the ssh of the git installation is preferred + String sshPath = getPathToExe("ssh"); + if (sshPath != null) { + sshexe = new File(sshPath); + if (sshexe.exists()) { + return sshexe; + } + } + throw new RuntimeException( "ssh executable not found. The git plugin only supports official git client https://git-scm.com/download/win"); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java new file mode 100644 index 0000000000..4476d18a28 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java @@ -0,0 +1,125 @@ +package org.jenkinsci.plugins.gitclient; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.jvnet.hudson.test.Issue; + +/** + * Tests the order in which {@link CliGitAPIImpl#getSSHExecutable()} searches for ssh. + * The ssh of the git installation must be preferred over an ssh found on the system PATH. + * + * @author Akash Manna + */ +class CliGitAPISshExecutableTest { + + @TempDir + private File tempDir; + + private CliGitAPIImpl gitClientWithEnv(String gitExe, Map environmentVariables) { + return new CliGitAPIImpl(gitExe, tempDir, null, null) { + @Override + String getEnvVar(String envVar) { + return environmentVariables.get(envVar); + } + }; + } + + /* Paths are joined with a backslash, so on Unix the file name contains backslashes */ + private File createExecutable(File executable) throws IOException { + File parent = executable.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + assertTrue(executable.createNewFile(), "Could not create " + executable); + return executable; + } + + private File pathDirectoryWithSsh() throws IOException { + File pathDir = new File(tempDir, "path-dir"); + pathDir.mkdirs(); + createExecutable(new File(pathDir, "ssh.exe")); + return pathDir; + } + + @Test + @Issue("JENKINS-1715") + void sshOfGitInstallationPreferredOverSshOnPath() throws Exception { + File programFiles = new File(tempDir, "Program Files"); + File gitSsh = createExecutable(new File(programFiles + "\\Git\\bin\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("ProgramFiles", programFiles.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-1715") + void sshOfGitInstallationInUsrBinPreferredOverSshOnPath() throws Exception { + File programFiles = new File(tempDir, "Program Files"); + File gitSsh = createExecutable(new File(programFiles + "\\Git\\usr\\bin\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("ProgramFiles", programFiles.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-1715") + void sshBesideGitExecutablePreferredOverSshOnPath() throws Exception { + File gitExe = new File(tempDir, "MinGit\\cmd\\git.exe"); + File gitSsh = createExecutable(new File(gitExe.getParent() + "\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv(gitExe.getAbsolutePath(), env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-72450") + void sshOnPathUsedWhenGitInstallationHasNoSsh() throws Exception { + File pathDir = pathDirectoryWithSsh(); + + Map env = new HashMap<>(); + env.put("PATH", pathDir.getAbsolutePath()); + + assertEquals( + new File(pathDir, "ssh.exe"), + gitClientWithEnv(new File(tempDir, "no-ssh-here\\git.exe").getAbsolutePath(), env) + .getSSHExecutable()); + } + + @Test + @Issue("JENKINS-72450") + void gitSshEnvironmentVariablePreferredOverSshOnPath() throws Exception { + File gitSshVariable = createExecutable(new File(tempDir, "custom-ssh.exe")); + + Map env = new HashMap<>(); + env.put("GIT_SSH", gitSshVariable.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSshVariable, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + void sshNotFoundReportsUnsupportedGitInstallation() { + Map env = new HashMap<>(); + env.put("PATH", new File(tempDir, "empty-dir").getAbsolutePath()); + + CliGitAPIImpl git = gitClientWithEnv(new File(tempDir, "no-ssh-here\\git.exe").getAbsolutePath(), env); + RuntimeException e = assertThrows(RuntimeException.class, git::getSSHExecutable); + assertTrue(e.getMessage().startsWith("ssh executable not found"), e.getMessage()); + } +}