/*
* 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.commands;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ExceptionUtil;
import com.intellij.vcsUtil.VcsFileUtil;
import git4idea.GitBranch;
import git4idea.push.GitPushSpec;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Collection of common native Git commands.
*
* @author Kirill Likhodedov
*/
public class Git {
private static final Logger LOG = Logger.getInstance(Git.class);
private Git() {
}
/**
* Calls 'git init' on the specified directory.
* // TODO use common format
*/
public static void init(Project project, VirtualFile root) throws VcsException {
GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.INIT);
h.setSilent(false);
h.setNoSSH(true);
h.run();
if (!h.errors().isEmpty()) {
throw h.errors().get(0);
}
}
/**
*
Queries Git for the unversioned files in the given paths.
*
* Note: this method doesn't check for ignored files. You have to check if the file is ignored afterwards, if needed.
*
*
* @param files files that are to be checked for the unversioned files among them.
* Pass null
to query the whole repository.
* @return Unversioned files from the given scope.
*/
@NotNull
public static Set untrackedFiles(@NotNull Project project,
@NotNull VirtualFile root,
@Nullable Collection files) throws VcsException {
final Set untrackedFiles = new HashSet();
if (files == null) {
untrackedFiles.addAll(untrackedFilesNoChunk(project, root, null));
} else {
for (List relativePaths : VcsFileUtil.chunkFiles(root, files)) {
untrackedFiles.addAll(untrackedFilesNoChunk(project, root, relativePaths));
}
}
return untrackedFiles;
}
// relativePaths are guaranteed to fit into command line length limitations.
@NotNull
private static Collection untrackedFilesNoChunk(@NotNull Project project,
@NotNull VirtualFile root,
@Nullable List relativePaths)
throws VcsException {
final Set untrackedFiles = new HashSet();
GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LS_FILES);
h.setNoSSH(true);
h.setSilent(true);
h.addParameters("--exclude-standard", "--others", "-z");
h.endOptions();
if (relativePaths != null) {
h.addParameters(relativePaths);
}
final String output = h.run();
if (StringUtil.isEmptyOrSpaces(output)) {
return untrackedFiles;
}
for (String relPath : output.split("\u0000")) {
VirtualFile f = root.findFileByRelativePath(relPath);
if (f == null) {
// files was created on disk, but VirtualFile hasn't yet been created,
// when the GitChangeProvider has already been requested about changes.
LOG.info(String.format("VirtualFile for path [%s] is null", relPath));
} else {
untrackedFiles.add(f);
}
}
return untrackedFiles;
}
@NotNull
public static GitCommandResult clone(@NotNull Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName) {
GitLineHandlerPasswordRequestAware handler = new GitLineHandlerPasswordRequestAware(project, parentDirectory, GitCommand.CLONE);
handler.addParameters(url);
handler.addParameters(clonedDirectoryName);
return run(handler, true);
}
/**
* {@code git checkout <reference>}
* {@code git checkout -b <newBranch> <reference>}
*/
public static GitCommandResult checkout(@NotNull GitRepository repository,
@NotNull String reference,
@Nullable String newBranch,
boolean force,
@NotNull GitLineHandlerListener... listeners) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
h.setSilent(false);
if (force) {
h.addParameters("--force");
}
if (newBranch == null) { // simply checkout
h.addParameters(reference);
}
else { // checkout reference as new branch
h.addParameters("-b", newBranch, reference);
}
for (GitLineHandlerListener listener : listeners) {
h.addLineListener(listener);
}
return run(h);
}
/**
* {@code git checkout -b <branchName>}
*/
public static GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName,
@Nullable GitLineHandlerListener listener) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
h.setSilent(false);
h.addParameters("-b");
h.addParameters(branchName);
if (listener != null) {
h.addLineListener(listener);
}
return run(h);
}
public static GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName,
@Nullable GitLineHandlerListener listener, String reference) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.TAG);
h.setSilent(false);
h.addParameters(tagName);
if (reference != null && ! reference.isEmpty()) {
h.addParameters(reference);
}
if (listener != null) {
h.addLineListener(listener);
}
return run(h);
}
/**
* {@code git branch -d } or {@code git branch -D }
*/
public static GitCommandResult branchDelete(@NotNull GitRepository repository,
@NotNull String branchName,
boolean force,
@NotNull GitLineHandlerListener... listeners) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
h.setSilent(false);
h.addParameters(force ? "-D" : "-d");
h.addParameters(branchName);
for (GitLineHandlerListener listener : listeners) {
h.addLineListener(listener);
}
return run(h);
}
/**
* Get branches containing the commit.
* {@code git branch --contains }
*/
@NotNull
public static GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
h.addParameters("--contains", commit);
return run(h);
}
/**
* Create branch without checking it out.
* {@code git branch }
*/
@NotNull
public static GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
h.addParameters(branchName);
return run(h);
}
/**
* Returns the last (tip) commit on the given branch.
* {@code git rev-list -1 }
*/
public static GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REV_LIST);
h.addParameters("-1");
h.addParameters(branchName);
return run(h);
}
public static GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull GitLineHandlerListener... listeners) {
final GitLineHandlerPasswordRequestAware h = new GitLineHandlerPasswordRequestAware(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
h.setSilent(false);
for (GitLineHandlerListener listener : listeners) {
h.addLineListener(listener);
}
GitRemote remote = pushSpec.getRemote();
h.addParameters(remote.getName());
GitBranch remoteBranch = pushSpec.getDest();
String destination = remoteBranch.getName().replaceFirst(remote.getName() + "/", "");
h.addParameters(pushSpec.getSource().getName() + ":" + destination);
return run(h, true);
}
private static GitCommandResult run(@NotNull GitLineHandler handler) {
return run(handler, false);
}
/**
* Runs the given {@link GitLineHandler} in the current thread and returns the {@link GitCommandResult}.
*/
private static GitCommandResult run(@NotNull GitLineHandler handler, boolean remote) {
handler.setNoSSH(!remote);
final List errorOutput = new ArrayList();
final List output = new ArrayList();
final AtomicInteger exitCode = new AtomicInteger();
final AtomicBoolean startFailed = new AtomicBoolean();
handler.addLineListener(new GitLineHandlerListener() {
@Override public void onLineAvailable(String line, Key outputType) {
if (isError(line)) {
errorOutput.add(line);
} else {
output.add(line);
}
}
@Override public void processTerminated(int code) {
exitCode.set(code);
}
@Override public void startFailed(Throwable exception) {
startFailed.set(true);
errorOutput.add("Failed to start Git process");
errorOutput.add(ExceptionUtil.getThrowableText(exception));
}
});
handler.runInCurrentThread(null);
if (handler instanceof GitLineHandlerPasswordRequestAware && ((GitLineHandlerPasswordRequestAware)handler).hadAuthRequest()) {
errorOutput.add("Authentication failed");
}
final boolean success = !startFailed.get() && errorOutput.isEmpty() && (handler.isIgnoredErrorCode(exitCode.get()) || exitCode.get() == 0);
return new GitCommandResult(success, exitCode.get(), errorOutput, output);
}
/**
* Check if the line looks line an error message
*/
private static boolean isError(String text) {
for (String indicator : ERROR_INDICATORS) {
if (text.startsWith(indicator.toLowerCase())) {
return true;
}
}
return false;
}
// could be upper-cased, so should check case-insensitively
private static final String[] ERROR_INDICATORS = {
"error", "fatal", "Cannot apply", "Could not", "Interactive rebase already started", "refusing to pull", "cannot rebase:", "conflict"
};
}