/* * Copyright 2000-2016 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.ExecutableValidator; 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.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import org.jetbrains.annotations.NotNull; import java.text.ParseException; import java.util.Objects; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The version of Git. Note that the version number ignores build and commit hash. * The class is able to distinct MSYS and CYGWIN Gits under Windows assuming that msysgit adds the 'msysgit' suffix to the output * of the 'git version' command. * This is not a very good way to distinguish msys and cygwin since in old versions of msys they didn't add the suffix. * * Note: this class has a natural ordering that is inconsistent with equals. */ public final class GitVersion implements Comparable<GitVersion> { /** * Type indicates the type of this git distribution: is it native (unix) or msys or cygwin. * Type UNDEFINED means that the type doesn't matter in certain condition. */ public enum Type { UNIX, MSYS, CYGWIN, /** The type doesn't matter or couldn't be detected. */ UNDEFINED, /** * Information about Git version is unavailable because the GitVcs hasn't fully initialized yet, or because Git executable is invalid. */ NULL } /** * The minimal supported version */ public static final GitVersion MIN = new GitVersion(1, 7, 1, 1); /** * Special version with a special Type which indicates, that Git version information is unavailable. * Probably, because of invalid executable, or when GitVcs hasn't fully initialized yet. */ public static final GitVersion NULL = new GitVersion(0, 0, 0, 0, Type.NULL); private static final Pattern FORMAT = Pattern.compile( "git version (\\d+)\\.(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(.*)", Pattern.CASE_INSENSITIVE); private static final Logger LOG = Logger.getInstance(GitVersion.class.getName()); private final int myMajor; private final int myMinor; private final int myRevision; private final int myPatchLevel; private final Type myType; private final int myHashCode; public GitVersion(int major, int minor, int revision, int patchLevel, Type type) { myMajor = major; myMinor = minor; myRevision = revision; myPatchLevel = patchLevel; myType = type; myHashCode = Objects.hash(myMajor, myMinor, myRevision, myPatchLevel); } /** * Creates new GitVersion with the UNDEFINED Type which actually means that the type doesn't matter for current purpose. */ public GitVersion(int major, int minor, int revision, int patchLevel) { this(major, minor, revision, patchLevel, Type.UNDEFINED); } /** * Parses output of "git version" command. */ @NotNull public static GitVersion parse(String output) throws ParseException { if (StringUtil.isEmptyOrSpaces(output)) { throw new ParseException("Empty git --version output: " + output, 0); } Matcher m = FORMAT.matcher(output.trim()); if (!m.matches()) { throw new ParseException("Unsupported format of git --version output: " + output, 0); } int major = getIntGroup(m, 1); int minor = getIntGroup(m, 2); int rev = getIntGroup(m, 3); int patch = getIntGroup(m, 4); boolean msys = (m.groupCount() >= 5) && m.group(5) != null && m.group(5).toLowerCase().contains("msysgit"); Type type; if (SystemInfo.isWindows) { type = msys ? Type.MSYS : Type.CYGWIN; } else { type = Type.UNIX; } return new GitVersion(major, minor, rev, patch, type); } // Utility method used in parsing - checks that the given capture group exists and captured something - then returns the captured value, // otherwise returns 0. private static int getIntGroup(Matcher matcher, int group) { if (group > matcher.groupCount()+1) { return 0; } final String match = matcher.group(group); if (match == null) { return 0; } return Integer.parseInt(match); } @NotNull public static GitVersion identifyVersion(String gitExecutable) throws TimeoutException, ExecutionException, ParseException { GeneralCommandLine commandLine = new GeneralCommandLine(); commandLine.setExePath(gitExecutable); commandLine.addParameter("--version"); commandLine.setCharset(CharsetToolkit.getDefaultSystemCharset()); CapturingProcessHandler handler = new CapturingProcessHandler(commandLine); ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); ProcessOutput result = indicator == null ? handler.runProcess(ExecutableValidator.TIMEOUT_MS) : handler.runProcessWithProgressIndicator(indicator); if (result.isTimeout()) { throw new TimeoutException("Couldn't identify the version of Git - stopped by timeout."); } else if (result.isCancelled()) { LOG.info("Cancelled by user. exitCode=" + result.getExitCode()); throw new ProcessCanceledException(); } else if (result.getExitCode() != 0 || !result.getStderr().isEmpty()) { LOG.info("getVersion exitCode=" + result.getExitCode() + " errors: " + result.getStderr()); // anyway trying to parse try { parse(result.getStdout()); } catch (ParseException pe) { throw new ExecutionException("Errors while executing git --version. exitCode=" + result.getExitCode() + " errors: " + result.getStderr()); } } return parse(result.getStdout()); } /** * @return true if the version is supported by the plugin */ public boolean isSupported() { return getType() != Type.NULL && compareTo(MIN) >= 0; } /** * Note: this class has a natural ordering that is inconsistent with equals. * Two GitVersions are equal if their number versions are equal and if their types are equal. * Types are considered equal also if one of them is undefined. Otherwise they are compared. */ @Override public boolean equals(final Object obj) { if (!(obj instanceof GitVersion)) { return false; } GitVersion other = (GitVersion) obj; if (compareTo(other) != 0) { return false; } if (myType == Type.UNDEFINED || other.myType == Type.UNDEFINED) { return true; } return myType == other.myType; } /** * Hashcode is computed from numbered components of the version. Thus GitVersions with the same numbered components will be compared * by equals, and there the type will be taken into consideration). */ @Override public int hashCode() { return myHashCode; } /** * Note: this class has a natural ordering that is inconsistent with equals. * Only numbered versions are compared, so * (msys git 1.7.3).compareTo(cygwin git 1.7.3) == 0 * BUT * (msys git 1.7.3).equals(cygwin git 1.7.3) == false * * {@link GitVersion#NULL} is less than any other not-NULL version. */ public int compareTo(@NotNull GitVersion o) { if (o.getType() == Type.NULL) { return (getType() == Type.NULL ? 0 : 1); } int d = myMajor - o.myMajor; if (d != 0) { return d; } d = myMinor - o.myMinor; if (d != 0) { return d; } d = myRevision - o.myRevision; if (d != 0) { return d; } return myPatchLevel - o.myPatchLevel; } @Override public String toString() { final String msysIndicator = (myType == Type.MSYS ? ".msysgit" : ""); return myMajor + "." + myMinor + "." + myRevision + "." + myPatchLevel + msysIndicator; } /** * @return true if this version is older or the same than the given one. */ public boolean isOlderOrEqual(final GitVersion gitVersion) { return gitVersion != null && compareTo(gitVersion) <= 0; } /** * @return true if this version is later or the same than the given one. */ public boolean isLaterOrEqual(GitVersion version) { return version != null && compareTo(version) >= 0; } public Type getType() { return myType; } public boolean isNull() { return getType() == Type.NULL; } }