blob: 4b0bdeadc8f9b71092de5afd3a84f2347454841a [file] [log] [blame]
/*
* Copyright 2000-2011 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.repo;
import com.intellij.dvcs.repo.RepoStateException;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitLocalBranch;
import git4idea.GitPlatformFacade;
import git4idea.GitRemoteBranch;
import git4idea.GitSvnRemoteBranch;
import git4idea.branch.GitBranchUtil;
import org.ini4j.Ini;
import org.ini4j.Profile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Reads information from the {@code .git/config} file, and parses it to actual objects.</p>
*
* <p>Currently doesn't read all the information: general information about remotes and branch tracking</p>
*
* <p>Parsing is performed with the help of <a href="http://ini4j.sourceforge.net/">ini4j</a> library.</p>
*
* TODO: note, that other git configuration files (such as ~/.gitconfig) are not handled yet.
*
* @author Kirill Likhodedov
*/
public class GitConfig {
/**
* Special remote typical for git-svn configuration:
* <pre>[branch "trunk]
* remote = .
* merge = refs/remotes/trunk
* </pre>
* @deprecated Use {@link GitSvnRemoteBranch}
*/
@Deprecated
public static final String DOT_REMOTE = ".";
private static final Logger LOG = Logger.getInstance(GitConfig.class);
private static final Pattern REMOTE_SECTION = Pattern.compile("(?:svn-)?remote \"(.*)\"");
private static final Pattern URL_SECTION = Pattern.compile("url \"(.*)\"");
private static final Pattern BRANCH_INFO_SECTION = Pattern.compile("branch \"(.*)\"");
private static final Pattern BRANCH_COMMON_PARAMS_SECTION = Pattern.compile("branch");
@NotNull private final Collection<Remote> myRemotes;
@NotNull private final Collection<Url> myUrls;
@NotNull private final Collection<BranchConfig> myTrackedInfos;
private GitConfig(@NotNull Collection<Remote> remotes, @NotNull Collection<Url> urls, @NotNull Collection<BranchConfig> trackedInfos) {
myRemotes = remotes;
myUrls = urls;
myTrackedInfos = trackedInfos;
}
/**
* <p>Returns Git remotes defined in {@code .git/config}.</p>
*
* <p>Remote is returned with all transformations (such as {@code pushUrl, url.<base>.insteadOf}) already applied to it.
* See {@link GitRemote} for details.</p>
*
* <p><b>Note:</b> remotes can be defined separately in {@code .git/remotes} directory, by creating a file for each remote with
* remote parameters written in the file. This method returns ONLY remotes defined in {@code .git/config}.</p>
* @return Git remotes defined in {@code .git/config}.
*/
@NotNull
Collection<GitRemote> parseRemotes() {
// populate GitRemotes with substituting urls when needed
return ContainerUtil.map(myRemotes, new Function<Remote, GitRemote>() {
@Override
public GitRemote fun(@Nullable Remote remote) {
assert remote != null;
return convertRemoteToGitRemote(myUrls, remote);
}
});
}
@NotNull
private static GitRemote convertRemoteToGitRemote(@NotNull Collection<Url> urls, @NotNull Remote remote) {
UrlsAndPushUrls substitutedUrls = substituteUrls(urls, remote);
return new GitRemote(remote.myName, substitutedUrls.getUrls(), substitutedUrls.getPushUrls(),
remote.getFetchSpecs(), computePushSpec(remote));
}
/**
* Create branch tracking information based on the information defined in {@code .git/config}.
*/
@NotNull
Collection<GitBranchTrackInfo> parseTrackInfos(@NotNull final Collection<GitLocalBranch> localBranches,
@NotNull final Collection<GitRemoteBranch> remoteBranches) {
return ContainerUtil.mapNotNull(myTrackedInfos, new Function<BranchConfig, GitBranchTrackInfo>() {
@Override
public GitBranchTrackInfo fun(BranchConfig config) {
if (config != null) {
return convertBranchConfig(config, localBranches, remoteBranches);
}
return null;
}
});
}
/**
* Creates an instance of GitConfig by reading information from the specified {@code .git/config} file.
* @throws RepoStateException if {@code .git/config} couldn't be read or has invalid format.<br/>
* If in general it has valid format, but some sections are invalid, it skips invalid sections, but reports an error.
*/
@NotNull
static GitConfig read(@NotNull GitPlatformFacade platformFacade, @NotNull File configFile) {
GitConfig emptyConfig = new GitConfig(Collections.<Remote>emptyList(), Collections.<Url>emptyList(),
Collections.<BranchConfig>emptyList());
if (!configFile.exists()) {
LOG.info("No .git/config file at " + configFile.getPath());
return emptyConfig;
}
Ini ini = new Ini();
ini.getConfig().setMultiOption(true); // duplicate keys (e.g. url in [remote])
ini.getConfig().setTree(false); // don't need tree structure: it corrupts url in section name (e.g. [url "http://github.com/"]
try {
ini.load(configFile);
}
catch (IOException e) {
LOG.warn(new RepoStateException("Couldn't load .git/config file at " + configFile.getPath(), e));
return emptyConfig;
}
IdeaPluginDescriptor plugin = platformFacade.getPluginByClassName(GitConfig.class.getName());
ClassLoader classLoader = plugin == null ? null : plugin.getPluginClassLoader(); // null if IDEA is started from IDEA
Pair<Collection<Remote>, Collection<Url>> remotesAndUrls = parseRemotes(ini, classLoader);
Collection<BranchConfig> trackedInfos = parseTrackedInfos(ini, classLoader);
return new GitConfig(remotesAndUrls.getFirst(), remotesAndUrls.getSecond(), trackedInfos);
}
@NotNull
private static Collection<BranchConfig> parseTrackedInfos(@NotNull Ini ini, @Nullable ClassLoader classLoader) {
Collection<BranchConfig> configs = new ArrayList<BranchConfig>();
for (Map.Entry<String, Profile.Section> stringSectionEntry : ini.entrySet()) {
String sectionName = stringSectionEntry.getKey();
Profile.Section section = stringSectionEntry.getValue();
if (sectionName.startsWith("branch")) {
BranchConfig branchConfig = parseBranchSection(sectionName, section, classLoader);
if (branchConfig != null) {
configs.add(branchConfig);
}
}
}
return configs;
}
@Nullable
private static GitBranchTrackInfo convertBranchConfig(@Nullable BranchConfig branchConfig,
@NotNull Collection<GitLocalBranch> localBranches,
@NotNull Collection<GitRemoteBranch> remoteBranches) {
if (branchConfig == null) {
return null;
}
final String branchName = branchConfig.getName();
String remoteName = branchConfig.getBean().getRemote();
String mergeName = branchConfig.getBean().getMerge();
String rebaseName = branchConfig.getBean().getRebase();
if (StringUtil.isEmptyOrSpaces(mergeName) && StringUtil.isEmptyOrSpaces(rebaseName)) {
LOG.info("No branch." + branchName + ".merge/rebase item in the .git/config");
return null;
}
if (StringUtil.isEmptyOrSpaces(remoteName)) {
LOG.info("No branch." + branchName + ".remote item in the .git/config");
return null;
}
boolean merge = mergeName != null;
final String remoteBranchName = (merge ? mergeName : rebaseName);
assert remoteName != null;
assert remoteBranchName != null;
GitLocalBranch localBranch = findLocalBranch(branchName, localBranches);
GitRemoteBranch remoteBranch = GitBranchUtil.findRemoteBranchByName(remoteBranchName, remoteName, remoteBranches);
if (localBranch == null || remoteBranch == null) {
return null;
}
return new GitBranchTrackInfo(localBranch, remoteBranch, merge);
}
@Nullable
private static GitLocalBranch findLocalBranch(@NotNull String branchName, @NotNull Collection<GitLocalBranch> localBranches) {
final String name = GitBranchUtil.stripRefsPrefix(branchName);
try {
return ContainerUtil.find(localBranches, new Condition<GitLocalBranch>() {
@Override
public boolean value(@Nullable GitLocalBranch input) {
assert input != null;
return input.getName().equals(name);
}
});
}
catch (NoSuchElementException e) {
LOG.info("Couldn't find branch with name " + name);
return null;
}
}
@Nullable
private static BranchConfig parseBranchSection(String sectionName, Profile.Section section, @Nullable ClassLoader classLoader) {
BranchBean branchBean = section.as(BranchBean.class, classLoader);
Matcher matcher = BRANCH_INFO_SECTION.matcher(sectionName);
if (matcher.matches()) {
return new BranchConfig(matcher.group(1), branchBean);
}
if (BRANCH_COMMON_PARAMS_SECTION.matcher(sectionName).matches()) {
LOG.debug(String.format("Common branch option(s) defined .git/config. sectionName: %s%n section: %s", sectionName, section));
return null;
}
LOG.error(String.format("Invalid branch section format in .git/config. sectionName: %s%n section: %s", sectionName, section));
return null;
}
@NotNull
private static Pair<Collection<Remote>, Collection<Url>> parseRemotes(@NotNull Ini ini, @Nullable ClassLoader classLoader) {
Collection<Remote> remotes = new ArrayList<Remote>();
Collection<Url> urls = new ArrayList<Url>();
for (Map.Entry<String, Profile.Section> stringSectionEntry : ini.entrySet()) {
String sectionName = stringSectionEntry.getKey();
Profile.Section section = stringSectionEntry.getValue();
if (sectionName.startsWith("remote") || sectionName.startsWith("svn-remote")) {
Remote remote = parseRemoteSection(sectionName, section, classLoader);
if (remote != null) {
remotes.add(remote);
}
}
else if (sectionName.startsWith("url")) {
Url url = parseUrlSection(sectionName, section, classLoader);
if (url != null) {
urls.add(url);
}
}
}
return Pair.create(remotes, urls);
}
@NotNull
private static List<String> computePushSpec(@NotNull Remote remote) {
List<String> pushSpec = remote.getPushSpec();
return pushSpec == null ? remote.getFetchSpecs() : pushSpec;
}
/**
* <p>
* Applies {@code url.<base>.insteadOf} and {@code url.<base>.pushInsteadOf} transformations to {@code url} and {@code pushUrl} of
* the given remote.
* </p>
* <p>
* The logic, is as follows:
* <ul>
* <li>If remote.url starts with url.insteadOf, it it substituted.</li>
* <li>If remote.pushUrl starts with url.insteadOf, it is substituted.</li>
* <li>If remote.pushUrl starts with url.pushInsteadOf, it is not substituted.</li>
* <li>If remote.url starts with url.pushInsteadOf, but remote.pushUrl is given, additional push url is not added.</li>
* </ul>
* </p>
*
* <p>
* TODO: if there are several matches in url sections, the longest should be applied. // currently only one is applied
* </p>
*
* <p>
* This is according to {@code man git-config ("url.<base>.insteadOf" and "url.<base>.pushInsteadOf" sections},
* {@code man git-push ("URLS" section)} and the following discussions in the Git mailing list:
* <a href="http://article.gmane.org/gmane.comp.version-control.git/183587">insteadOf override urls and pushUrls</a>,
* <a href="http://thread.gmane.org/gmane.comp.version-control.git/127910">pushInsteadOf doesn't override explicit pushUrl</a>.
* </p>
*/
@NotNull
private static UrlsAndPushUrls substituteUrls(@NotNull Collection<Url> urlSections, @NotNull Remote remote) {
List<String> urls = new ArrayList<String>(remote.getUrls().size());
Collection<String> pushUrls = new ArrayList<String>();
// urls are substituted by insteadOf
// if there are no pushUrls, we create a pushUrl for pushInsteadOf substitutions
for (final String remoteUrl : remote.getUrls()) {
boolean substituted = false;
for (Url url : urlSections) {
String insteadOf = url.getInsteadOf();
String pushInsteadOf = url.getPushInsteadOf();
// null means no entry, i.e. nothing to substitute. Empty string means substituting everything
if (insteadOf != null && remoteUrl.startsWith(insteadOf)) {
urls.add(substituteUrl(remoteUrl, url, insteadOf));
substituted = true;
break;
}
else if (pushInsteadOf != null && remoteUrl.startsWith(pushInsteadOf)) {
if (remote.getPushUrls().isEmpty()) { // only if there are no explicit pushUrls
pushUrls.add(substituteUrl(remoteUrl, url, pushInsteadOf)); // pushUrl is different
}
urls.add(remoteUrl); // but url is left intact
substituted = true;
break;
}
}
if (!substituted) {
urls.add(remoteUrl);
}
}
// pushUrls are substituted only by insteadOf, not by pushInsteadOf
for (final String remotePushUrl : remote.getPushUrls()) {
boolean substituted = false;
for (Url url : urlSections) {
String insteadOf = url.getInsteadOf();
// null means no entry, i.e. nothing to substitute. Empty string means substituting everything
if (insteadOf != null && remotePushUrl.startsWith(insteadOf)) {
pushUrls.add(substituteUrl(remotePushUrl, url, insteadOf));
substituted = true;
break;
}
}
if (!substituted) {
pushUrls.add(remotePushUrl);
}
}
// if no pushUrls are explicitly defined yet via pushUrl or url.<base>.pushInsteadOf, they are the same as urls.
if (pushUrls.isEmpty()) {
pushUrls = new ArrayList<String>(urls);
}
return new UrlsAndPushUrls(urls, pushUrls);
}
private static class UrlsAndPushUrls {
final List<String> myUrls;
final Collection<String> myPushUrls;
private UrlsAndPushUrls(List<String> urls, Collection<String> pushUrls) {
myPushUrls = pushUrls;
myUrls = urls;
}
public Collection<String> getPushUrls() {
return myPushUrls;
}
public List<String> getUrls() {
return myUrls;
}
}
@NotNull
private static String substituteUrl(@NotNull String remoteUrl, @NotNull Url url, @NotNull String insteadOf) {
return url.myName + remoteUrl.substring(insteadOf.length());
}
@Nullable
private static Remote parseRemoteSection(@NotNull String sectionName, @NotNull Profile.Section section, @Nullable ClassLoader classLoader) {
RemoteBean remoteBean = section.as(RemoteBean.class, classLoader);
Matcher matcher = REMOTE_SECTION.matcher(sectionName);
if (matcher.matches()) {
return new Remote(matcher.group(1), remoteBean);
}
LOG.error(String.format("Invalid remote section format in .git/config. sectionName: %s section: %s", sectionName, section));
return null;
}
@Nullable
private static Url parseUrlSection(@NotNull String sectionName, @NotNull Profile.Section section, @Nullable ClassLoader classLoader) {
UrlBean urlBean = section.as(UrlBean.class, classLoader);
Matcher matcher = URL_SECTION.matcher(sectionName);
if (matcher.matches()) {
return new Url(matcher.group(1), urlBean);
}
LOG.error(String.format("Invalid url section format in .git/config. sectionName: %s section: %s", sectionName, section));
return null;
}
private static class Remote {
private final String myName;
private final RemoteBean myRemoteBean;
private Remote(@NotNull String name, @NotNull RemoteBean remoteBean) {
myRemoteBean = remoteBean;
myName = name;
}
@NotNull
private Collection<String> getUrls() {
return nonNullCollection(myRemoteBean.getUrl());
}
@NotNull
private Collection<String> getPushUrls() {
return nonNullCollection(myRemoteBean.getPushUrl());
}
@Nullable
// no need in wrapping null here - we check for it in #computePushSpec
private List<String> getPushSpec() {
String[] push = myRemoteBean.getPush();
return push == null ? null : Arrays.asList(push);
}
@NotNull
private List<String> getFetchSpecs() {
return Arrays.asList(notNull(myRemoteBean.getFetch()));
}
}
private interface RemoteBean {
@Nullable String[] getFetch();
@Nullable String[] getPush();
@Nullable String[] getUrl();
@Nullable String[] getPushUrl();
}
private static class Url {
private final String myName;
private final UrlBean myUrlBean;
private Url(String name, UrlBean urlBean) {
myUrlBean = urlBean;
myName = name;
}
@Nullable
// null means to entry, i.e. nothing to substitute. Empty string means substituting everything
public String getInsteadOf() {
return myUrlBean.getInsteadOf();
}
@Nullable
// null means to entry, i.e. nothing to substitute. Empty string means substituting everything
public String getPushInsteadOf() {
return myUrlBean.getPushInsteadOf();
}
}
private interface UrlBean {
@Nullable String getInsteadOf();
@Nullable String getPushInsteadOf();
}
private static class BranchConfig {
private final String myName;
private final BranchBean myBean;
public BranchConfig(String name, BranchBean bean) {
myName = name;
myBean = bean;
}
public String getName() {
return myName;
}
public BranchBean getBean() {
return myBean;
}
}
private interface BranchBean {
@Nullable String getRemote();
@Nullable String getMerge();
@Nullable String getRebase();
}
@NotNull
private static String[] notNull(@Nullable String[] s) {
return s == null ? ArrayUtil.EMPTY_STRING_ARRAY : s;
}
@NotNull
private static Collection<String> nonNullCollection(@Nullable String[] array) {
return array == null ? Collections.<String>emptyList() : new ArrayList<String>(Arrays.asList(array));
}
}