diff --git a/plugins/git4idea/src/git4idea/GitVcs.java b/plugins/git4idea/src/git4idea/GitVcs.java
index ae255e990959..12ab13e48880 100644
--- a/plugins/git4idea/src/git4idea/GitVcs.java
+++ b/plugins/git4idea/src/git4idea/GitVcs.java
@@ -85,6 +85,7 @@ import git4idea.vfs.GitVFSListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
@@ -339,11 +340,7 @@ public class GitVcs extends AbstractVcs {
@Override
protected void activate() {
- if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
- if (myExecutableValidator.checkExecutableAndNotifyIfNeeded()) {
- checkVersion();
- }
- }
+ checkExecutableAndVersion();
if (myVFSListener == null) {
myVFSListener = new GitVFSListener(myProject, this, myGit);
@@ -364,6 +361,27 @@ public class GitVcs extends AbstractVcs {
((GitCommitsSequentialIndex) ServiceManager.getService(GitCommitsSequentially.class)).activate();
}
+ private void checkExecutableAndVersion() {
+ boolean executableIsAlreadyCheckedAndFine = false;
+ String pathToGit = myAppSettings.getPathToGitAtStartup();
+ if (pathToGit == null) {
+ myAppSettings.setPathToGit(new GitExecutableDetector().detect());
+ }
+ else if (!pathToGit.contains(File.separator)) { // no path, just sole executable, with a hope that it is in path
+ // subject to redetect the path if executable validator fails
+ if (!myExecutableValidator.isExecutableValid()) {
+ myAppSettings.setPathToGit(new GitExecutableDetector().detect());
+ }
+ else {
+ executableIsAlreadyCheckedAndFine = true; // not to check it twice
+ }
+ }
+
+ if (executableIsAlreadyCheckedAndFine || myExecutableValidator.checkExecutableAndNotifyIfNeeded()) {
+ checkVersion();
+ }
+ }
+
@Override
protected void deactivate() {
if (myVFSListener != null) {
diff --git a/plugins/git4idea/src/git4idea/config/GitExecutableDetector.java b/plugins/git4idea/src/git4idea/config/GitExecutableDetector.java
new file mode 100644
index 000000000000..6495773e71b7
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/config/GitExecutableDetector.java
@@ -0,0 +1,261 @@
+/*
+ * 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.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";
+
+ @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 = checkMsys();
+ if (exec != null) {
+ return exec;
+ }
+
+ exec = checkCygwin();
+ if (exec != null) {
+ return exec;
+ }
+
+ return checkSoleExecutable();
+ }
+
+ @Nullable
+ private static String checkMsys() {
+ final String[] PROGRAM_FILES = { "Program Files", "Program Files (x86)" };
+
+ // collecting all potential msys distributives
+ List distrs = new ArrayList();
+ 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;
+ }
+ }
+
+ // Compare strategy: greater is better (if v1 > v2, then v1 is a better candidate for the Git executable)
+ private static class VersionDirsComparator implements Comparator {
+
+ @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);
+ }
+ }
+}
diff --git a/plugins/git4idea/src/git4idea/config/GitVcsApplicationSettings.java b/plugins/git4idea/src/git4idea/config/GitVcsApplicationSettings.java
index 23c18e83a8ad..97aa12c33624 100644
--- a/plugins/git4idea/src/git4idea/config/GitVcsApplicationSettings.java
+++ b/plugins/git4idea/src/git4idea/config/GitVcsApplicationSettings.java
@@ -16,13 +16,9 @@
package git4idea.config;
import com.intellij.openapi.components.*;
-import com.intellij.openapi.util.SystemInfo;
-import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.io.File;
-
/**
* The application wide settings for the git
*/
@@ -31,17 +27,7 @@ import java.io.File;
storages = {@Storage(file = StoragePathMacros.APP_CONFIG + "/vcs.xml")})
public class GitVcsApplicationSettings implements PersistentStateComponent {
- @NonNls private static final String[] DEFAULT_WINDOWS_PATHS = { "C:\\Program Files\\Git\\bin",
- "C:\\Program Files (x86)\\Git\\bin",
- "C:\\cygwin\\bin" };
- @NonNls private static final String[] DEFAULT_UNIX_PATHS = { "/usr/local/bin",
- "/usr/bin",
- "/opt/local/bin",
- "/opt/bin",
- "/usr/local/git/bin" };
- @NonNls private static final String[] DEFAULT_WINDOWS_GITS = { "git.cmd", "git.exe" };
- @NonNls private static final String DEFAULT_UNIX_GIT = "git";
-
+
private State myState = new State();
/**
@@ -70,42 +56,22 @@ public class GitVcsApplicationSettings implements PersistentStateComponentThis method differs from {@link #getPathToGit()} only in the @Nullable annotation: initially the path can be null,
+ * but after VCS is initialized for the first time, correct path is set in {@link git4idea.GitVcs} and is never null,
+ * so we want the @NotNull annotation there.
+ * This method should be called only from {@link git4idea.GitVcs#activate()}.
+ *
+ * @return Path to Git or null if the path was not written to the config yet.
+ */
+ @Nullable
+ public String getPathToGitAtStartup() {
+ return myState.myPathToGit;
}
public void setPathToGit(String pathToGit) {
diff --git a/plugins/git4idea/tests/git4idea/config/GitExecutableDetectorWindowsTest.groovy b/plugins/git4idea/tests/git4idea/config/GitExecutableDetectorWindowsTest.groovy
new file mode 100644
index 000000000000..a8a565fb578e
--- /dev/null
+++ b/plugins/git4idea/tests/git4idea/config/GitExecutableDetectorWindowsTest.groovy
@@ -0,0 +1,169 @@
+/*
+ * 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.openapi.util.io.FileUtil
+import git4idea.test.GitTestUtil
+import org.jetbrains.annotations.NotNull
+import org.junit.Before
+import org.junit.Test
+
+import java.lang.reflect.Field
+import java.lang.reflect.Modifier
+
+import static junit.framework.Assert.assertEquals
+
+/**
+ *
+ * @author Kirill Likhodedov
+ */
+class GitExecutableDetectorWindowsTest {
+
+ private File testRoot
+ private def canRun = []
+
+ @Before
+ void setUp() {
+ GitTestUtil.setWindows(true);
+ testRoot = FileUtil.createTempDirectory("", "")
+ setWindowsRoot(testRoot);
+ }
+
+ @Test
+ void "Simple case"() {
+ fs "/Program Files/Git/bin/git.exe"
+ assertExecutable "C:/Program Files/Git/bin/git.exe"
+ }
+
+ @Test
+ void "Prefer default Git without version to versioned ones"() {
+ fs "/Program Files/Git/bin/git.exe", "/Program Files/Git 1.8/bin/git.exe", "/Program Files/Git_1.7.1/bin/git.exe"
+ assertExecutable "C:/Program Files/Git/bin/git.exe"
+ }
+
+ @Test
+ void "Prefer the latest version"() {
+ fs "/Program Files (x86)/Git 1.8/cmd/git.exe",
+ "/Program Files/Git_1.7.1/bin/git.exe",
+ "/Program Files/Git_1.7.5/cmd/git.cmd",
+ "/Program Files (x86)/Git_1.7.0.2/bin/git.exe"
+ assertExecutable "C:/Program Files (x86)/Git 1.8/cmd/git.exe"
+ }
+
+ @Test
+ void "Prefer Program Files over x86"() {
+ fs "/Program Files (x86)/Git 1.8/bin/git.exe", "/Program Files/Git 1.8/bin/git.exe"
+ assertExecutable "C:/Program Files/Git 1.8/bin/git.exe"
+ }
+
+ @Test
+ void "Prefer git.cmd over git.exe"() {
+ fs "/Program Files (x86)/Git 1.7.4/bin/git.exe", "/Program Files/Git 1.7.4/cmd/git.cmd"
+ assertExecutable "C:/Program Files/Git 1.7.4/cmd/git.cmd"
+ }
+
+ @Test
+ void "Prefer cmd over bin in newer versions of Git"() {
+ fs "/Program Files (x86)/Git 1.8/bin/git.exe", "/Program Files/Git 1.8/cmd/git.exe"
+ assertExecutable "C:/Program Files/Git 1.8/cmd/git.exe"
+ }
+
+ @Test
+ void "1.8.0 Prefer cmd/git.cmd over cmd/git.exe and bin/git.exe"() {
+ fs "/Program Files (x86)/Git_1.8/bin/git.exe",
+ "/Program Files (x86)/Git_1.8/cmd/git.cmd",
+ "/Program Files (x86)/Git_1.8/cmd/git.exe"
+ assertExecutable "C:/Program Files (x86)/Git_1.8/cmd/git.cmd"
+ }
+
+ @Test
+ void "Prefer msys over cygwin"() {
+ fs "/Program Files (x86)/Git_1.8.0.2/cmd/git.exe",
+ "/Program Files (x86)/Git_1.8.0.2/bin/git.exe",
+ "/cygwin/bin/git.exe"
+ assertExecutable "C:/Program Files (x86)/Git_1.8.0.2/cmd/git.exe"
+ }
+
+ @Test
+ void "Only cygwin"() {
+ fs "/cygwin/bin/git.exe"
+ assertExecutable "C:/cygwin/bin/git.exe"
+ }
+
+ @Test
+ void "Many different versions, real case"() {
+ fs "/Program Files (x86)/Git_1.7.0.2/bin/git.exe",
+ "/Program Files (x86)/Git_1.7.0.2/cmd/git.cmd",
+ "/Program Files (x86)/Git_1.7.8/bin/git.exe",
+ "/Program Files (x86)/Git_1.7.8/cmd/git.cmd",
+ "/Program Files (x86)/Git_1.8/bin/git.exe",
+ "/Program Files (x86)/Git_1.8/cmd/git.cmd",
+ "/Program Files (x86)/Git_1.8/cmd/git.exe",
+ "/Program Files (x86)/Git_1.8.0.2/cmd/git.exe",
+ "/Program Files (x86)/Git_1.8.0.2/bin/git.exe",
+ "/cygwin/bin/git.exe"
+ assertExecutable "C:/Program Files (x86)/Git_1.8.0.2/cmd/git.exe"
+ }
+
+ @Test
+ void "Program not found, try git.exe"() {
+ canRun = [ "git.exe" ]
+ assertExecutable "git.exe"
+ }
+
+ @Test
+ void "For both git.exe and git.cmd prefer git.cmd"() {
+ canRun = [ "git.exe", "git.cmd" ]
+ assertExecutable "git.cmd"
+ }
+
+ def assertExecutable(String expected) {
+ expected = FileUtil.toSystemDependentName(expected).replace("C:", testRoot.path)
+ assertEquals "Incorrect executable detected", expected, detect()
+ }
+
+ def fs(String... paths) {
+ paths.each {
+ mkPath(it)
+ }
+ }
+
+ def mkPath(String path) {
+ def file = new File(testRoot.getPath() + FileUtil.toSystemDependentName(path))
+ file.getParentFile().mkdirs()
+ file.createNewFile()
+ }
+
+ private String detect() {
+ new GitExecutableDetector() {
+ @Override
+ protected boolean runs(@NotNull String exec) {
+ return canRun.contains(exec);
+ }
+ }.detect();
+ }
+
+ static def setWindowsRoot(File file) {
+ Field field = GitExecutableDetector.class.getDeclaredField("WIN_ROOT");
+ field.setAccessible(true);
+
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+ field.set(null, file);
+ }
+
+}