blob: 8a556d6826eb30eef3760fc9edc367d3481d9f56 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.config;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileFilter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Tries to detect the path to Git executable.
*
* @author Kirill Likhodedov
*/
public class GitExecutableDetector {
private static final Logger LOG = Logger.getInstance(GitExecutableDetector.class);
private static final String[] UNIX_PATHS = { "/usr/local/bin",
"/usr/bin",
"/opt/local/bin",
"/opt/bin",
"/usr/local/git/bin"};
private static final String UNIX_EXECUTABLE = "git";
private static final File WIN_ROOT = new File("C:"); // the constant is extracted to be able to create files in "Program Files" in tests
private static final String GIT_CMD = "git.cmd";
private static final String GIT_EXE = "git.exe";
public static final String DEFAULT_WIN_GIT = GIT_EXE;
public static final String PATH_ENV = "PATH";
@NotNull
public String detect() {
if (SystemInfo.isWindows) {
return detectForWindows();
}
return detectForUnix();
}
@NotNull
private static String detectForUnix() {
for (String p : UNIX_PATHS) {
File f = new File(p, UNIX_EXECUTABLE);
if (f.exists()) {
return f.getPath();
}
}
return UNIX_EXECUTABLE;
}
@NotNull
private String detectForWindows() {
String exec = checkInPath();
if (exec != null) {
return exec;
}
exec = checkProgramFiles();
if (exec != null) {
return exec;
}
exec = checkCygwin();
if (exec != null) {
return exec;
}
return checkSoleExecutable();
}
/**
* Looks into the %PATH% and checks Git directories mentioned there.
* @return Git executable to be used or null if nothing interesting was found in the PATH.
*/
@Nullable
private String checkInPath() {
String PATH = getPath();
if (PATH == null) {
return null;
}
List<String> pathEntries = StringUtil.split(PATH, ";");
for (String pathEntry : pathEntries) {
if (looksLikeGit(pathEntry)) {
return checkBinDir(new File(pathEntry));
}
}
return null;
}
private static boolean looksLikeGit(@NotNull String path) {
List<String> dirs = FileUtil.splitPath(path);
for (String dir : dirs) {
if (dir.toLowerCase().startsWith("git")) {
return true;
}
}
return false;
}
@Nullable
private static String checkProgramFiles() {
final String[] PROGRAM_FILES = { "Program Files", "Program Files (x86)" };
// collecting all potential msys distributives
List<File> distrs = new ArrayList<File>();
for (String programFiles : PROGRAM_FILES) {
File pf = new File(WIN_ROOT, programFiles);
File[] children = pf.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() && pathname.getName().toLowerCase().startsWith("git");
}
});
if (!pf.exists() || children == null) {
continue;
}
distrs.addAll(Arrays.asList(children));
}
// greater is better => sorting in the descending order to match the best version first, when iterating
Collections.sort(distrs, Collections.reverseOrder(new VersionDirsComparator()));
for (File distr : distrs) {
String exec = checkDistributive(distr);
if (exec != null) {
return exec;
}
}
return null;
}
@Nullable
private static String checkCygwin() {
final String[] OTHER_WINDOWS_PATHS = { FileUtil.toSystemDependentName("cygwin/bin/git.exe") };
for (String otherPath : OTHER_WINDOWS_PATHS) {
File file = new File(WIN_ROOT, otherPath);
if (file.exists()) {
return file.getPath();
}
}
return null;
}
@NotNull
private String checkSoleExecutable() {
if (runs(GIT_CMD)) {
return GIT_CMD;
}
return GIT_EXE;
}
@Nullable
private static String checkDistributive(@Nullable File gitDir) {
if (gitDir == null || !gitDir.exists()) {
return null;
}
final String[] binDirs = { "cmd", "bin" };
for (String binDir : binDirs) {
String exec = checkBinDir(new File(gitDir, binDir));
if (exec != null) {
return exec;
}
}
return null;
}
@Nullable
private static String checkBinDir(@NotNull File binDir) {
if (!binDir.exists()) {
return null;
}
for (String exec : new String[]{ GIT_CMD, GIT_EXE }) {
File fe = new File(binDir, exec);
if (fe.exists()) {
return fe.getPath();
}
}
return null;
}
/**
* Checks if it is possible to run the specified program.
* Made protected for tests not to start a process there.
*/
protected boolean runs(@NotNull String exec) {
GeneralCommandLine commandLine = new GeneralCommandLine();
commandLine.setExePath(exec);
try {
CapturingProcessHandler handler = new CapturingProcessHandler(commandLine.createProcess(), CharsetToolkit.getDefaultSystemCharset());
ProcessOutput result = handler.runProcess((int)TimeUnit.SECONDS.toMillis(5));
return !result.isTimeout();
}
catch (ExecutionException e) {
return false;
}
}
@Nullable
protected String getPath() {
return System.getenv(PATH_ENV);
}
// Compare strategy: greater is better (if v1 > v2, then v1 is a better candidate for the Git executable)
private static class VersionDirsComparator implements Comparator<File> {
@Override
public int compare(File f1, File f2) {
String name1 = f1.getName().toLowerCase();
String name2 = f2.getName().toLowerCase();
// C:\Program Files\Git is better candidate for _default_ than C:\Program Files\Git_1.8.0
if (name1.equals("git")) {
return name2.equals("git") ? fallback(f1, f2) : 1;
}
else if (name2.equals("git")) {
return -1;
}
final Pattern GIT_WITH_VERSION = Pattern.compile("^git[ _]*([\\d\\.]*).*$");
Matcher m1 = GIT_WITH_VERSION.matcher(name1);
Matcher m2 = GIT_WITH_VERSION.matcher(name2);
if (m1.matches() && m2.matches()) {
GitVersion v1 = parseGitVersion(m1.group(1));
GitVersion v2 = parseGitVersion(m2.group(1));
if (v1 == null || v2 == null) {
return fallback(f1, f2);
}
int compareVersions = v1.compareTo(v2);
return compareVersions == 0 ? fallback(f1, f2) : compareVersions;
}
return fallback(f1, f2);
}
private static int fallback(@NotNull File f1, @NotNull File f2) {
// "Program Files" is preferable over "Program Files (x86)"
int compareParents = f1.getParentFile().getName().compareTo(f2.getParentFile().getName());
if (compareParents != 0) {
return -compareParents; // greater is better => reversing
}
// probably some unrecognized format of Git directory naming => just compare lexicographically
String name1 = f1.getName().toLowerCase();
String name2 = f2.getName().toLowerCase();
return name1.compareTo(name2);
}
// not using GitVersion#parse(), because it requires at least 3 items in the version (1.7.3),
// and parses the `git version` command output, not just the version string.
@Nullable
private static GitVersion parseGitVersion(@Nullable String name) {
if (name == null) {
return null;
}
final Pattern VERSION = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?.*");
Matcher m = VERSION.matcher(name);
if (!m.matches()) {
return null;
}
try {
int major = Integer.parseInt(m.group(1));
return new GitVersion(major, parseOrNull(m.group(2)), parseOrNull(m.group(3)), parseOrNull(m.group(4)));
}
catch (NumberFormatException e) {
LOG.info("Unexpected NFE when parsing [" + name + "]", e);
return null;
}
}
private static int parseOrNull(String group) {
return group == null ? 0 : Integer.parseInt(group);
}
}
}