blob: fec8b0b8c2c688c285837dbdb4cb9a3c8d1a449a [file] [log] [blame]
/*
* Copyright 2000-2009 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;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.FilePathsHelper;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcsUtil.VcsFileUtil;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.changes.GitCommittedChangeList;
import git4idea.commands.*;
import git4idea.config.GitConfigUtil;
import git4idea.i18n.GitBundle;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.util.GitUIUtil;
import git4idea.util.StringScanner;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.*;
/**
* Git utility/helper methods
*/
public class GitUtil {
/**
* Comparator for virtual files by name
*/
public static final Comparator<VirtualFile> VIRTUAL_FILE_COMPARATOR = new Comparator<VirtualFile>() {
public int compare(final VirtualFile o1, final VirtualFile o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
if (o1.getPresentableUrl() == null) {
return -1;
}
if (o2.getPresentableUrl() == null) {
return 1;
}
return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
}
};
/**
* The UTF-8 encoding name
*/
public static final String UTF8_ENCODING = "UTF-8";
/**
* The UTF8 charset
*/
public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
public static final String DOT_GIT = ".git";
private final static Logger LOG = Logger.getInstance(GitUtil.class);
private static final int SHORT_HASH_LENGTH = 8;
public static final Predicate<GitBranchTrackInfo> NOT_NULL_PREDICATE = new Predicate<GitBranchTrackInfo>() {
@Override
public boolean apply(@Nullable GitBranchTrackInfo input) {
return input != null;
}
};
/**
* A private constructor to suppress instance creation
*/
private GitUtil() {
// do nothing
}
@Nullable
public static VirtualFile findGitDir(@NotNull VirtualFile rootDir) {
VirtualFile child = rootDir.findChild(DOT_GIT);
if (child == null) {
return null;
}
if (child.isDirectory()) {
return child;
}
// this is standard for submodules, although probably it can
String content;
try {
content = readFile(child);
}
catch (IOException e) {
throw new RuntimeException("Couldn't read " + child, e);
}
String pathToDir;
String prefix = "gitdir:";
if (content.startsWith(prefix)) {
pathToDir = content.substring(prefix.length()).trim();
}
else {
pathToDir = content;
}
if (!FileUtil.isAbsolute(pathToDir)) {
String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootDir.getPath(), pathToDir));
if (canonicalPath == null) {
return null;
}
pathToDir = FileUtil.toSystemIndependentName(canonicalPath);
}
return VcsUtil.getVirtualFileWithRefresh(new File(pathToDir));
}
/**
* Makes 3 attempts to get the contents of the file. If all 3 fail with an IOException, rethrows the exception.
*/
@NotNull
public static String readFile(@NotNull VirtualFile file) throws IOException {
final int ATTEMPTS = 3;
for (int attempt = 0; attempt < ATTEMPTS; attempt++) {
try {
return new String(file.contentsToByteArray());
}
catch (IOException e) {
LOG.info(String.format("IOException while reading %s (attempt #%s)", file, attempt));
if (attempt >= ATTEMPTS - 1) {
throw e;
}
}
}
throw new AssertionError("Shouldn't get here. Couldn't read " + file);
}
/**
* Sort files by Git root
*
* @param virtualFiles files to sort
* @return sorted files
* @throws VcsException if non git files are passed
*/
@NotNull
public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
return sortFilesByGitRoot(virtualFiles, false);
}
/**
* Sort files by Git root
*
* @param virtualFiles files to sort
* @param ignoreNonGit if true, non-git files are ignored
* @return sorted files
* @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
*/
public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
throws VcsException {
Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
for (VirtualFile file : virtualFiles) {
final VirtualFile vcsRoot = gitRootOrNull(file);
if (vcsRoot == null) {
if (ignoreNonGit) {
continue;
}
else {
throw new VcsException("The file " + file.getPath() + " is not under Git");
}
}
List<VirtualFile> files = result.get(vcsRoot);
if (files == null) {
files = new ArrayList<VirtualFile>();
result.put(vcsRoot, files);
}
files.add(file);
}
return result;
}
/**
* Sort files by vcs root
*
* @param files files to sort.
* @return the map from root to the files under the root
* @throws VcsException if non git files are passed
*/
public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
return sortFilePathsByGitRoot(files, false);
}
/**
* Sort files by vcs root
*
* @param files files to sort.
* @return the map from root to the files under the root
*/
public static Map<VirtualFile, List<FilePath>> sortGitFilePathsByGitRoot(Collection<FilePath> files) {
try {
return sortFilePathsByGitRoot(files, true);
}
catch (VcsException e) {
throw new RuntimeException("Unexpected exception:", e);
}
}
/**
* Sort files by vcs root
*
* @param files files to sort.
* @param ignoreNonGit if true, non-git files are ignored
* @return the map from root to the files under the root
* @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
*/
@NotNull
public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(@NotNull Collection<FilePath> files, boolean ignoreNonGit)
throws VcsException {
Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
for (FilePath p : files) {
VirtualFile root = getGitRootOrNull(p);
if (root == null) {
if (ignoreNonGit) {
continue;
}
else {
throw new VcsException("The file " + p.getPath() + " is not under Git");
}
}
List<FilePath> l = rc.get(root);
if (l == null) {
l = new ArrayList<FilePath>();
rc.put(root, l);
}
l.add(p);
}
return rc;
}
/**
* Parse UNIX timestamp as it is returned by the git
*
* @param value a value to parse
* @return timestamp as {@link Date} object
*/
private static Date parseTimestamp(String value) {
final long parsed;
parsed = Long.parseLong(value.trim());
return new Date(parsed * 1000);
}
/**
* Parse UNIX timestamp returned from Git and handle {@link NumberFormatException} if one happens: return new {@link Date} and
* log the error properly.
* In some cases git output gets corrupted and this method is intended to catch the reason, why.
* @param value Value to parse.
* @param handler Git handler that was called to received the output.
* @param gitOutput Git output.
* @return Parsed Date or <code>new Date</code> in the case of error.
*/
public static Date parseTimestampWithNFEReport(String value, GitHandler handler, String gitOutput) {
try {
return parseTimestamp(value);
} catch (NumberFormatException e) {
LOG.error("annotate(). NFE. Handler: " + handler + ". Output: " + gitOutput, e);
return new Date();
}
}
/**
* Get git roots from content roots
*
* @param roots git content roots
* @return a content root
*/
public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
for (VirtualFile root : roots) {
VirtualFile f = root;
do {
if (f.findFileByRelativePath(DOT_GIT) != null) {
rc.add(f);
break;
}
f = f.getParent();
}
while (f != null);
}
return rc;
}
/**
* Return a git root for the file path (the parent directory with ".git" subdirectory)
*
* @param filePath a file path
* @return git root for the file
* @throws IllegalArgumentException if the file is not under git
* @throws VcsException if the file is not under git
*
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
public static VirtualFile getGitRoot(@NotNull FilePath filePath) throws VcsException {
VirtualFile root = getGitRootOrNull(filePath);
if (root != null) {
return root;
}
throw new VcsException("The file " + filePath + " is not under git.");
}
/**
* Return a git root for the file path (the parent directory with ".git" subdirectory)
*
* @param filePath a file path
* @return git root for the file or null if the file is not under git
*
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
@Deprecated
@Nullable
public static VirtualFile getGitRootOrNull(@NotNull final FilePath filePath) {
return getGitRootOrNull(filePath.getIOFile());
}
public static boolean isGitRoot(final File file) {
return file != null && file.exists() && file.isDirectory() && new File(file, DOT_GIT).exists();
}
/**
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
@Deprecated
@Nullable
public static VirtualFile getGitRootOrNull(final File file) {
File root = file;
while (root != null && (!root.exists() || !root.isDirectory() || !new File(root, DOT_GIT).exists())) {
root = root.getParentFile();
}
return root == null ? null : LocalFileSystem.getInstance().findFileByIoFile(root);
}
/**
* Return a git root for the file (the parent directory with ".git" subdirectory)
*
* @param file the file to check
* @return git root for the file
* @throws VcsException if the file is not under git
*
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
final VirtualFile root = gitRootOrNull(file);
if (root != null) {
return root;
}
else {
throw new VcsException("The file " + file.getPath() + " is not under git.");
}
}
/**
* Return a git root for the file (the parent directory with ".git" subdirectory)
*
* @param file the file to check
* @return git root for the file or null if the file is not not under Git
*
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
@Nullable
public static VirtualFile gitRootOrNull(final VirtualFile file) {
if (file instanceof AbstractVcsVirtualFile) {
return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
}
VirtualFile root = file;
while (root != null) {
if (root.findFileByRelativePath(DOT_GIT) != null) {
return root;
}
root = root.getParent();
}
return root;
}
/**
* Get git roots for the project. The method shows dialogs in the case when roots cannot be retrieved, so it should be called
* from the event dispatch thread.
*
* @param project the project
* @param vcs the git Vcs
* @return the list of the roots
*
* @deprecated because uses the java.io.File.
* @use GitRepositoryManager#getRepositoryForFile().
*/
@NotNull
public static List<VirtualFile> getGitRoots(Project project, GitVcs vcs) throws VcsException {
final VirtualFile[] contentRoots = ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(vcs);
if (contentRoots == null || contentRoots.length == 0) {
throw new VcsException(GitBundle.getString("repository.action.missing.roots.unconfigured.message"));
}
final List<VirtualFile> roots = new ArrayList<VirtualFile>(gitRootsForPaths(Arrays.asList(contentRoots)));
if (roots.size() == 0) {
throw new VcsException(GitBundle.getString("repository.action.missing.roots.misconfigured"));
}
Collections.sort(roots, VIRTUAL_FILE_COMPARATOR);
return roots;
}
/**
* Check if the virtual file under git
*
* @param vFile a virtual file
* @return true if the file is under git
*/
public static boolean isUnderGit(final VirtualFile vFile) {
return gitRootOrNull(vFile) != null;
}
/**
* Return committer name based on author name and committer name
*
* @param authorName the name of author
* @param committerName the name of committer
* @return just a name if they are equal, or name that includes both author and committer
*/
public static String adjustAuthorName(final String authorName, String committerName) {
if (!authorName.equals(committerName)) {
//noinspection HardCodedStringLiteral
committerName = authorName + ", via " + committerName;
}
return committerName;
}
/**
* Check if the file path is under git
*
* @param path the path
* @return true if the file path is under git
*/
public static boolean isUnderGit(final FilePath path) {
return getGitRootOrNull(path) != null;
}
/**
* Get git roots for the selected paths
*
* @param filePaths the context paths
* @return a set of git roots
*/
public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
for (FilePath path : filePaths) {
final VirtualFile root = getGitRootOrNull(path);
if (root != null) {
rc.add(root);
}
}
return rc;
}
/**
* Get git time (UNIX time) basing on the date object
*
* @param time the time to convert
* @return the time in git format
*/
public static String gitTime(Date time) {
long t = time.getTime() / 1000;
return Long.toString(t);
}
/**
* Format revision number from long to 16-digit abbreviated revision
*
* @param rev the abbreviated revision number as long
* @return the revision string
*/
public static String formatLongRev(long rev) {
return String.format("%015x%x", (rev >>> 4), rev & 0xF);
}
public static void getLocalCommittedChanges(final Project project,
final VirtualFile root,
final Consumer<GitSimpleHandler> parametersSpecifier,
final Consumer<GitCommittedChangeList> consumer, boolean skipDiffsForMerge) throws VcsException {
GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
h.setSilent(true);
h.addParameters("--pretty=format:%x04%x01" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
parametersSpecifier.consume(h);
String output = h.run();
LOG.debug("getLocalCommittedChanges output: '" + output + "'");
StringScanner s = new StringScanner(output);
final StringBuilder sb = new StringBuilder();
boolean firstStep = true;
while (s.hasMoreData()) {
final String line = s.line();
final boolean lineIsAStart = line.startsWith("\u0004\u0001");
if ((!firstStep) && lineIsAStart) {
final StringScanner innerScanner = new StringScanner(sb.toString());
sb.setLength(0);
consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
}
sb.append(lineIsAStart ? line.substring(2) : line).append('\n');
firstStep = false;
}
if (sb.length() > 0) {
final StringScanner innerScanner = new StringScanner(sb.toString());
sb.setLength(0);
consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
}
if (s.hasMoreData()) {
throw new IllegalStateException("More input is avaialble: " + s.line());
}
}
public static List<GitCommittedChangeList> getLocalCommittedChanges(final Project project,
final VirtualFile root,
final Consumer<GitSimpleHandler> parametersSpecifier)
throws VcsException {
final List<GitCommittedChangeList> rc = new ArrayList<GitCommittedChangeList>();
getLocalCommittedChanges(project, root, parametersSpecifier, new Consumer<GitCommittedChangeList>() {
public void consume(GitCommittedChangeList committedChangeList) {
rc.add(committedChangeList);
}
}, false);
return rc;
}
/**
* <p>Unescape path returned by Git.</p>
* <p>
* If there are quotes in the file name, Git not only escapes them, but also encloses the file name into quotes:
* <code>"\"quote"</code>
* </p>
* <p>
* If there are spaces in the file name, Git displays the name as is, without escaping spaces and without enclosing name in quotes.
* </p>
*
* @param path a path to unescape
* @return unescaped path ready to be searched in the VFS or file system.
* @throws com.intellij.openapi.vcs.VcsException if the path in invalid
*/
@NotNull
public static String unescapePath(@NotNull String path) throws VcsException {
final String QUOTE = "\"";
if (path.startsWith(QUOTE) && path.endsWith(QUOTE)) {
path = path.substring(1, path.length() - 1);
}
final int l = path.length();
StringBuilder rc = new StringBuilder(l);
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == '\\') {
//noinspection AssignmentToForLoopParameter
i++;
if (i >= l) {
throw new VcsException("Unterminated escape sequence in the path: " + path);
}
final char e = path.charAt(i);
switch (e) {
case '\\':
rc.append('\\');
break;
case 't':
rc.append('\t');
break;
case 'n':
rc.append('\n');
break;
case '"':
rc.append('"');
break;
default:
if (VcsFileUtil.isOctal(e)) {
// collect sequence of characters as a byte array.
// count bytes first
int n = 0;
for (int j = i; j < l;) {
if (VcsFileUtil.isOctal(path.charAt(j))) {
n++;
for (int k = 0; k < 3 && j < l && VcsFileUtil.isOctal(path.charAt(j)); k++) {
//noinspection AssignmentToForLoopParameter
j++;
}
}
if (j + 1 >= l || path.charAt(j) != '\\' || !VcsFileUtil.isOctal(path.charAt(j + 1))) {
break;
}
//noinspection AssignmentToForLoopParameter
j++;
}
// convert to byte array
byte[] b = new byte[n];
n = 0;
while (i < l) {
if (VcsFileUtil.isOctal(path.charAt(i))) {
int code = 0;
for (int k = 0; k < 3 && i < l && VcsFileUtil.isOctal(path.charAt(i)); k++) {
code = code * 8 + (path.charAt(i) - '0');
//noinspection AssignmentToForLoopParameter
i++;
}
b[n++] = (byte)code;
}
if (i + 1 >= l || path.charAt(i) != '\\' || !VcsFileUtil.isOctal(path.charAt(i + 1))) {
break;
}
//noinspection AssignmentToForLoopParameter
i++;
}
assert n == b.length;
// add them to string
final String encoding = GitConfigUtil.getFileNameEncoding();
try {
rc.append(new String(b, encoding));
}
catch (UnsupportedEncodingException e1) {
throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
}
}
else {
throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
}
}
}
else {
rc.append(c);
}
}
return rc.toString();
}
public static boolean justOneGitRepository(Project project) {
if (project.isDisposed()) {
return true;
}
GitRepositoryManager manager = getRepositoryManager(project);
return !manager.moreThanOneRoot();
}
@Nullable
public static GitRemote findRemoteByName(@NotNull GitRepository repository, @Nullable String name) {
if (name == null) {
return null;
}
for (GitRemote remote : repository.getRemotes()) {
if (remote.getName().equals(name)) {
return remote;
}
}
return null;
}
/**
* @deprecated Calls Git for tracked info, use {@link GitRepository#getBranchTrackInfos()} instead.
*/
@Nullable
@Deprecated
public static Pair<GitRemote, GitRemoteBranch> findMatchingRemoteBranch(GitRepository repository, GitLocalBranch branch)
throws VcsException {
/*
from man git-push:
git push
Works like git push <remote>, where <remote> is the current branch's remote (or origin, if no
remote is configured for the current branch).
*/
String remoteName = GitBranchUtil.getTrackedRemoteName(repository.getProject(), repository.getRoot(), branch.getName());
GitRemote remote;
if (remoteName == null) {
remote = findOrigin(repository.getRemotes());
} else {
remote = findRemoteByName(repository, remoteName);
}
if (remote == null) {
return null;
}
for (GitRemoteBranch remoteBranch : repository.getBranches().getRemoteBranches()) {
if (remoteBranch.getName().equals(remote.getName() + "/" + branch.getName())) {
return Pair.create(remote, remoteBranch);
}
}
return null;
}
@Nullable
private static GitRemote findOrigin(Collection<GitRemote> remotes) {
for (GitRemote remote : remotes) {
if (remote.getName().equals("origin")) {
return remote;
}
}
return null;
}
public static boolean repoContainsRemoteBranch(@NotNull GitRepository repository, @NotNull GitRemoteBranch dest) {
return repository.getBranches().getRemoteBranches().contains(dest);
}
@NotNull
public static Collection<VirtualFile> getRootsFromRepositories(@NotNull Collection<GitRepository> repositories) {
Collection<VirtualFile> roots = new ArrayList<VirtualFile>(repositories.size());
for (GitRepository repository : repositories) {
roots.add(repository.getRoot());
}
return roots;
}
@NotNull
public static Collection<GitRepository> getRepositoriesFromRoots(@NotNull GitRepositoryManager repositoryManager,
@NotNull Collection<VirtualFile> roots) {
Collection<GitRepository> repositories = new ArrayList<GitRepository>(roots.size());
for (VirtualFile root : roots) {
GitRepository repo = repositoryManager.getRepositoryForRoot(root);
if (repo == null) {
LOG.error("Repository not found for root " + root);
}
else {
repositories.add(repo);
}
}
return repositories;
}
/**
* Returns absolute paths which have changed remotely comparing to the current branch, i.e. performs
* <code>git diff --name-only master..origin/master</code>
*/
@NotNull
public static Collection<String> getPathsDiffBetweenRefs(@NotNull Git git, @NotNull GitRepository repository,
@NotNull String beforeRef, @NotNull String afterRef) throws VcsException {
List<String> parameters = Arrays.asList("--name-only", "--pretty=format:");
String range = beforeRef + ".." + afterRef;
GitCommandResult result = git.diff(repository, parameters, range);
if (!result.success()) {
LOG.info(String.format("Couldn't get diff in range [%s] for repository [%s]", range, repository.toLogString()));
return Collections.emptyList();
}
final Collection<String> remoteChanges = new HashSet<String>();
for (StringScanner s = new StringScanner(result.getOutputAsJoinedString()); s.hasMoreData(); ) {
final String relative = s.line();
if (StringUtil.isEmptyOrSpaces(relative)) {
continue;
}
final String path = repository.getRoot().getPath() + "/" + unescapePath(relative);
remoteChanges.add(FilePathsHelper.convertPath(path));
}
return remoteChanges;
}
@NotNull
public static GitRepositoryManager getRepositoryManager(@NotNull Project project) {
return ServiceManager.getService(project, GitRepositoryManager.class);
}
@Nullable
public static GitRepository getRepositoryForRootOrLogError(@NotNull Project project, @NotNull VirtualFile root) {
GitRepositoryManager manager = getRepositoryManager(project);
GitRepository repository = manager.getRepositoryForRoot(root);
if (repository == null) {
LOG.error("Repository is null for root " + root);
}
return repository;
}
@NotNull
public static String getPrintableRemotes(@NotNull Collection<GitRemote> remotes) {
return StringUtil.join(remotes, new Function<GitRemote, String>() {
@Override
public String fun(GitRemote remote) {
return remote.getName() + ": [" + StringUtil.join(remote.getUrls(), ", ") + "]";
}
}, "\n");
}
@NotNull
public static String getShortHash(@NotNull String hash) {
if (hash.length() == 0) return "";
if (hash.length() == 40) return hash.substring(0, SHORT_HASH_LENGTH);
if (hash.length() > 40) // revision string encoded with date too
{
return hash.substring(hash.indexOf("[") + 1, SHORT_HASH_LENGTH);
}
return hash;
}
@NotNull
public static String fileOrFolder(@NotNull VirtualFile file) {
if (file.isDirectory()) {
return "Folder";
}
else {
return "File";
}
}
/**
* Show changes made in the specified revision.
*
* @param project the project
* @param revision the revision number
* @param file the file affected by the revision
* @param local pass true to let the diff be editable, i.e. making the revision "at the right" be a local (current) revision.
* pass false to let both sides of the diff be non-editable.
* @param revertable pass true to let "Revert" action be active.
*/
public static void showSubmittedFiles(final Project project, final String revision, final VirtualFile file,
final boolean local, final boolean revertable) {
new Task.Backgroundable(project, GitBundle.message("changes.retrieving", revision)) {
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
try {
VirtualFile vcsRoot = getGitRoot(file);
final CommittedChangeList changeList = GitChangeUtils.getRevisionChanges(project, vcsRoot, revision, true, local, revertable);
if (changeList != null) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
AbstractVcsHelper.getInstance(project).showChangesListBrowser(changeList,
GitBundle.message("paths.affected.title", revision));
}
});
}
}
catch (final VcsException e) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
GitUIUtil.showOperationError(project, e, "git show");
}
});
}
}
}.queue();
}
/**
* Returns the tracking information (remote and the name of the remote branch), or null if we are not on a branch.
*/
@Nullable
public static GitBranchTrackInfo getTrackInfoForCurrentBranch(@NotNull GitRepository repository) {
GitLocalBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
return null;
}
return GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
}
@NotNull
public static Collection<GitRepository> getRepositoriesForFiles(@NotNull Project project, @NotNull Collection<VirtualFile> files) {
final GitRepositoryManager manager = getRepositoryManager(project);
com.google.common.base.Function<VirtualFile,GitRepository> ROOT_TO_REPO =
new com.google.common.base.Function<VirtualFile, GitRepository>() {
@Override
public GitRepository apply(@Nullable VirtualFile root) {
return root != null ? manager.getRepositoryForRoot(root) : null;
}
};
return Collections2.filter(Collections2.transform(sortFilesByGitRootsIgnoringOthers(files).keySet(), ROOT_TO_REPO),
Predicates.notNull());
}
@NotNull
public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRootsIgnoringOthers(@NotNull Collection<VirtualFile> files) {
try {
return sortFilesByGitRoot(files, true);
}
catch (VcsException e) {
LOG.error("Should never happen, since we passed 'ignore non-git' parameter", e);
return Collections.emptyMap();
}
}
/**
* git diff --name-only [--cached]
* @return true if there is anything in the unstaged/staging area, false if the unstraed/staging area is empty.
* @param staged if true checks the staging area, if false checks unstaged files.
* @param project
* @param root
*/
public static boolean hasLocalChanges(boolean staged, Project project, VirtualFile root) throws VcsException {
final GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF);
diff.addParameters("--name-only");
if (staged) {
diff.addParameters("--cached");
}
diff.setStdoutSuppressed(true);
diff.setStderrSuppressed(true);
diff.setSilent(true);
final String output = diff.run();
return !output.trim().isEmpty();
}
}