blob: a28025dfeef81cdb4604ccf3037ca22313562726 [file] [log] [blame]
/*
* Copyright 2000-2010 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.intellij.dvcs.DvcsUtil;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.*;
import com.intellij.notification.impl.NotificationsConfigurationImpl;
import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.impl.patch.formove.FilePathComparator;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeProvider;
import com.intellij.openapi.vcs.changes.CommitExecutor;
import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager;
import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
import com.intellij.openapi.vcs.diff.DiffProvider;
import com.intellij.openapi.vcs.diff.RevisionSelector;
import com.intellij.openapi.vcs.history.VcsHistoryProvider;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.merge.MergeProvider;
import com.intellij.openapi.vcs.rollback.RollbackEnvironment;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.openapi.vcs.update.UpdateEnvironment;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ComparatorDelegate;
import com.intellij.util.containers.Convertor;
import com.intellij.util.ui.UIUtil;
import git4idea.annotate.GitAnnotationProvider;
import git4idea.annotate.GitRepositoryForAnnotationsListener;
import git4idea.changes.GitCommittedChangeListProvider;
import git4idea.changes.GitOutgoingChangesProvider;
import git4idea.checkin.GitCheckinEnvironment;
import git4idea.checkin.GitCommitAndPushExecutor;
import git4idea.checkout.GitCheckoutProvider;
import git4idea.commands.Git;
import git4idea.config.*;
import git4idea.diff.GitDiffProvider;
import git4idea.diff.GitTreeDiffProvider;
import git4idea.history.GitHistoryProvider;
import git4idea.history.NewGitUsersComponent;
import git4idea.history.browser.GitHeavyCommit;
import git4idea.history.browser.GitProjectLogManager;
import git4idea.history.wholeTree.GitCommitDetailsProvider;
import git4idea.history.wholeTree.GitCommitsSequentialIndex;
import git4idea.history.wholeTree.GitCommitsSequentially;
import git4idea.i18n.GitBundle;
import git4idea.merge.GitMergeProvider;
import git4idea.rollback.GitRollbackEnvironment;
import git4idea.roots.GitIntegrationEnabler;
import git4idea.roots.GitRootChecker;
import git4idea.roots.GitRootDetectInfo;
import git4idea.roots.GitRootDetector;
import git4idea.status.GitChangeProvider;
import git4idea.ui.branch.GitBranchWidget;
import git4idea.update.GitUpdateEnvironment;
import git4idea.vfs.GitVFSListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Git VCS implementation
*/
public class GitVcs extends AbstractVcs<CommittedChangeList> {
public static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup(
"Git Messages", ChangesViewContentManager.TOOLWINDOW_ID, true);
public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup(
"Git Important Messages", NotificationDisplayType.STICKY_BALLOON, true);
public static final NotificationGroup MINOR_NOTIFICATION = new NotificationGroup(
"Git Minor Notifications", NotificationDisplayType.BALLOON, true);
static {
NotificationsConfigurationImpl.remove("Git");
}
public static final String NAME = "Git";
/**
* Provide selected Git commit in some commit list. Use this, when {@link Change} is not enough.
* @see VcsDataKeys#CHANGES
* @see #SELECTED_COMMITS
*/
public static final DataKey<GitHeavyCommit> GIT_COMMIT = DataKey.create("Git.Commit");
/**
* Provides the list of Git commits selected in some list, for example, in the Git log.
* @see #GIT_COMMIT
*/
public static final DataKey<List<GitHeavyCommit>> SELECTED_COMMITS = DataKey.create("Git.Selected.Commits");
/**
* Provides the possibility to receive on demand those commit details which usually are not accessible from the {@link git4idea.history.browser.GitHeavyCommit} object.
*/
public static final DataKey<GitCommitDetailsProvider> COMMIT_DETAILS_PROVIDER = DataKey.create("Git.Commits.Details.Provider");
private static final Logger log = Logger.getInstance(GitVcs.class.getName());
private static final VcsKey ourKey = createKey(NAME);
private final ChangeProvider myChangeProvider;
private final GitCheckinEnvironment myCheckinEnvironment;
private final RollbackEnvironment myRollbackEnvironment;
private final GitUpdateEnvironment myUpdateEnvironment;
private final GitAnnotationProvider myAnnotationProvider;
private final DiffProvider myDiffProvider;
private final VcsHistoryProvider myHistoryProvider;
@NotNull private final Git myGit;
private final ProjectLevelVcsManager myVcsManager;
private final GitVcsApplicationSettings myAppSettings;
private final Configurable myConfigurable;
private final RevisionSelector myRevSelector;
private final GitCommittedChangeListProvider myCommittedChangeListProvider;
private final @NotNull GitPlatformFacade myPlatformFacade;
private GitVFSListener myVFSListener; // a VFS listener that tracks file addition, deletion, and renaming.
private final ReadWriteLock myCommandLock = new ReentrantReadWriteLock(true); // The command read/write lock
private final TreeDiffProvider myTreeDiffProvider;
private final GitCommitAndPushExecutor myCommitAndPushExecutor;
private final GitExecutableValidator myExecutableValidator;
private GitBranchWidget myBranchWidget;
private GitVersion myVersion = GitVersion.NULL; // version of Git which this plugin uses.
private static final int MAX_CONSOLE_OUTPUT_SIZE = 10000;
private GitRepositoryForAnnotationsListener myRepositoryForAnnotationsListener;
@Nullable
public static GitVcs getInstance(Project project) {
if (project == null || project.isDisposed()) {
return null;
}
return (GitVcs) ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME);
}
public GitVcs(@NotNull Project project, @NotNull Git git,
@NotNull final ProjectLevelVcsManager gitVcsManager,
@NotNull final GitAnnotationProvider gitAnnotationProvider,
@NotNull final GitDiffProvider gitDiffProvider,
@NotNull final GitHistoryProvider gitHistoryProvider,
@NotNull final GitRollbackEnvironment gitRollbackEnvironment,
@NotNull final GitVcsApplicationSettings gitSettings,
@NotNull final GitVcsSettings gitProjectSettings) {
super(project, NAME);
myGit = git;
myVcsManager = gitVcsManager;
myAppSettings = gitSettings;
myChangeProvider = project.isDefault() ? null : ServiceManager.getService(project, GitChangeProvider.class);
myCheckinEnvironment = project.isDefault() ? null : ServiceManager.getService(project, GitCheckinEnvironment.class);
myAnnotationProvider = gitAnnotationProvider;
myDiffProvider = gitDiffProvider;
myHistoryProvider = gitHistoryProvider;
myRollbackEnvironment = gitRollbackEnvironment;
myRevSelector = new GitRevisionSelector();
myConfigurable = new GitVcsConfigurable(gitProjectSettings, myProject);
myUpdateEnvironment = new GitUpdateEnvironment(myProject, this, gitProjectSettings);
myCommittedChangeListProvider = new GitCommittedChangeListProvider(myProject);
myOutgoingChangesProvider = new GitOutgoingChangesProvider(myProject);
myTreeDiffProvider = new GitTreeDiffProvider(myProject);
myCommitAndPushExecutor = new GitCommitAndPushExecutor(myCheckinEnvironment);
myExecutableValidator = new GitExecutableValidator(myProject, this);
myPlatformFacade = ServiceManager.getService(myProject, GitPlatformFacade.class);
}
public ReadWriteLock getCommandLock() {
return myCommandLock;
}
/**
* Run task in background using the common queue (per project)
* @param task the task to run
*/
public static void runInBackground(Task.Backgroundable task) {
task.queue();
}
@Override
public CommittedChangesProvider getCommittedChangesProvider() {
return myCommittedChangeListProvider;
}
@Override
public String getRevisionPattern() {
// return the full commit hash pattern, possibly other revision formats should be supported as well
return "[0-9a-fA-F]+";
}
@Override
@NotNull
public CheckinEnvironment createCheckinEnvironment() {
return myCheckinEnvironment;
}
@NotNull
@Override
public MergeProvider getMergeProvider() {
return GitMergeProvider.detect(myProject);
}
@Override
@NotNull
public RollbackEnvironment createRollbackEnvironment() {
return myRollbackEnvironment;
}
@Override
@NotNull
public VcsHistoryProvider getVcsHistoryProvider() {
return myHistoryProvider;
}
@Override
public VcsHistoryProvider getVcsBlockHistoryProvider() {
return myHistoryProvider;
}
@Override
@NotNull
public String getDisplayName() {
return NAME;
}
@Override
@Nullable
public UpdateEnvironment createUpdateEnvironment() {
return myUpdateEnvironment;
}
@Override
@NotNull
public GitAnnotationProvider getAnnotationProvider() {
return myAnnotationProvider;
}
@Override
@NotNull
public DiffProvider getDiffProvider() {
return myDiffProvider;
}
@Override
@Nullable
public RevisionSelector getRevisionSelector() {
return myRevSelector;
}
@Override
@Nullable
public VcsRevisionNumber parseRevisionNumber(@Nullable String revision, @Nullable FilePath path) throws VcsException {
if (revision == null || revision.length() == 0) return null;
if (revision.length() > 40) { // date & revision-id encoded string
String dateString = revision.substring(0, revision.indexOf("["));
String rev = revision.substring(revision.indexOf("[") + 1, 40);
Date d = new Date(Date.parse(dateString));
return new GitRevisionNumber(rev, d);
}
if (path != null) {
try {
VirtualFile root = GitUtil.getGitRoot(path);
return GitRevisionNumber.resolve(myProject, root, revision);
}
catch (VcsException e) {
log.info("Unexpected problem with resolving the git revision number: ", e);
throw e;
}
}
return new GitRevisionNumber(revision);
}
@Override
@Nullable
public VcsRevisionNumber parseRevisionNumber(@Nullable String revision) throws VcsException {
return parseRevisionNumber(revision, null);
}
@Override
public boolean isVersionedDirectory(VirtualFile dir) {
return dir.isDirectory() && GitUtil.gitRootOrNull(dir) != null;
}
@Override
public VcsRootChecker getRootChecker() {
return new GitRootChecker(myProject, myPlatformFacade);
}
@Override
protected void start() throws VcsException {
}
@Override
protected void shutdown() throws VcsException {
}
@Override
protected void activate() {
checkExecutableAndVersion();
if (myVFSListener == null) {
myVFSListener = new GitVFSListener(myProject, this, myGit);
}
NewGitUsersComponent.getInstance(myProject).activate();
if (!Registry.is("git.new.log")) {
GitProjectLogManager.getInstance(myProject).activate();
}
if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
myBranchWidget = new GitBranchWidget(myProject);
DvcsUtil.installStatusBarWidget(myProject, myBranchWidget);
}
if (myRepositoryForAnnotationsListener == null) {
myRepositoryForAnnotationsListener = new GitRepositoryForAnnotationsListener(myProject);
}
((GitCommitsSequentialIndex) ServiceManager.getService(GitCommitsSequentially.class)).activate();
}
private void checkExecutableAndVersion() {
boolean executableIsAlreadyCheckedAndFine = false;
String pathToGit = myAppSettings.getPathToGit();
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) {
Disposer.dispose(myVFSListener);
myVFSListener = null;
}
NewGitUsersComponent.getInstance(myProject).deactivate();
GitProjectLogManager.getInstance(myProject).deactivate();
if (myBranchWidget != null) {
DvcsUtil.removeStatusBarWidget(myProject, myBranchWidget);
myBranchWidget = null;
}
((GitCommitsSequentialIndex) ServiceManager.getService(GitCommitsSequentially.class)).deactivate();
}
@NotNull
@Override
public synchronized Configurable getConfigurable() {
return myConfigurable;
}
@Nullable
public ChangeProvider getChangeProvider() {
return myChangeProvider;
}
/**
* Show errors as popup and as messages in vcs view.
*
* @param list a list of errors
* @param action an action
*/
public void showErrors(@NotNull List<VcsException> list, @NotNull String action) {
if (list.size() > 0) {
StringBuilder buffer = new StringBuilder();
buffer.append("\n");
buffer.append(GitBundle.message("error.list.title", action));
for (final VcsException exception : list) {
buffer.append("\n");
buffer.append(exception.getMessage());
}
final String msg = buffer.toString();
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog(myProject, msg, GitBundle.getString("error.dialog.title"));
}
});
}
}
/**
* Shows a plain message in the Version Control Console.
*/
public void showMessages(@NotNull String message) {
if (message.length() == 0) return;
showMessage(message, ConsoleViewContentType.NORMAL_OUTPUT.getAttributes());
}
/**
* Show message in the Version Control Console
* @param message a message to show
* @param style a style to use
*/
private void showMessage(@NotNull String message, final TextAttributes style) {
if (message.length() > MAX_CONSOLE_OUTPUT_SIZE) {
message = message.substring(0, MAX_CONSOLE_OUTPUT_SIZE);
}
myVcsManager.addMessageToConsoleWindow(message, style);
}
/**
* Checks Git version and updates the myVersion variable.
* In the case of exception or unsupported version reports the problem.
* Note that unsupported version is also applied - some functionality might not work (we warn about that), but no need to disable at all.
*/
public void checkVersion() {
final String executable = myAppSettings.getPathToGit();
try {
myVersion = GitVersion.identifyVersion(executable);
if (! myVersion.isSupported()) {
log.info("Unsupported Git version: " + myVersion);
final String SETTINGS_LINK = "settings";
final String UPDATE_LINK = "update";
String message = String.format("The <a href='" + SETTINGS_LINK + "'>configured</a> version of Git is not supported: %s.<br/> " +
"The minimal supported version is %s. Please <a href='" + UPDATE_LINK + "'>update</a>.",
myVersion, GitVersion.MIN);
IMPORTANT_ERROR_NOTIFICATION.createNotification("Unsupported Git version", message, NotificationType.ERROR,
new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) {
if (SETTINGS_LINK.equals(e.getDescription())) {
ShowSettingsUtil.getInstance().showSettingsDialog(myProject, getConfigurable().getDisplayName());
}
else if (UPDATE_LINK.equals(e.getDescription())) {
BrowserUtil.browse("http://git-scm.com");
}
}
}).notify(myProject);
}
} catch (Exception e) {
if (getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { // check executable before notifying error
final String reason = (e.getCause() != null ? e.getCause() : e).getMessage();
String message = GitBundle.message("vcs.unable.to.run.git", executable, reason);
if (!myProject.isDefault()) {
showMessage(message, ConsoleViewContentType.SYSTEM_OUTPUT.getAttributes());
}
VcsBalloonProblemNotifier.showOverVersionControlView(myProject, message, MessageType.ERROR);
}
}
}
/**
* @return the version number of Git, which is used by IDEA. Or {@link GitVersion#NULL} if version info is unavailable yet.
*/
@NotNull
public GitVersion getVersion() {
return myVersion;
}
/**
* Shows a command line message in the Version Control Console
*/
public void showCommandLine(final String cmdLine) {
SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss.SSS");
showMessage(f.format(new Date()) + ": " + cmdLine, ConsoleViewContentType.SYSTEM_OUTPUT.getAttributes());
}
/**
* Shows error message in the Version Control Console
*/
public void showErrorMessages(final String line) {
showMessage(line, ConsoleViewContentType.ERROR_OUTPUT.getAttributes());
}
@Override
public boolean allowsNestedRoots() {
return true;
}
@Override
public <S> List<S> filterUniqueRoots(final List<S> in, final Convertor<S, VirtualFile> convertor) {
Collections.sort(in, new ComparatorDelegate<S, VirtualFile>(convertor, FilePathComparator.getInstance()));
for (int i = 1; i < in.size(); i++) {
final S sChild = in.get(i);
final VirtualFile child = convertor.convert(sChild);
final VirtualFile childRoot = GitUtil.gitRootOrNull(child);
if (childRoot == null) {
// non-git file actually, skip it
continue;
}
for (int j = i - 1; j >= 0; --j) {
final S sParent = in.get(j);
final VirtualFile parent = convertor.convert(sParent);
// the method check both that parent is an ancestor of the child and that they share common git root
if (VfsUtilCore.isAncestor(parent, child, false) && VfsUtilCore.isAncestor(childRoot, parent, false)) {
in.remove(i);
//noinspection AssignmentToForLoopParameter
--i;
break;
}
}
}
return in;
}
@Override
public RootsConvertor getCustomConvertor() {
return GitRootConverter.INSTANCE;
}
public static VcsKey getKey() {
return ourKey;
}
@Override
public VcsType getType() {
return VcsType.distributed;
}
private final VcsOutgoingChangesProvider<CommittedChangeList> myOutgoingChangesProvider;
@Override
protected VcsOutgoingChangesProvider<CommittedChangeList> getOutgoingProviderImpl() {
return myOutgoingChangesProvider;
}
@Override
public RemoteDifferenceStrategy getRemoteDifferenceStrategy() {
return RemoteDifferenceStrategy.ASK_TREE_PROVIDER;
}
@Override
protected TreeDiffProvider getTreeDiffProviderImpl() {
return myTreeDiffProvider;
}
@Override
public List<CommitExecutor> getCommitExecutors() {
return Collections.<CommitExecutor>singletonList(myCommitAndPushExecutor);
}
@NotNull
public GitExecutableValidator getExecutableValidator() {
return myExecutableValidator;
}
@Override
public boolean fileListenerIsSynchronous() {
return false;
}
@Override
@CalledInAwt
public void enableIntegration() {
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
public void run() {
GitRootDetectInfo detectInfo = new GitRootDetector(myProject, myPlatformFacade).detect();
new GitIntegrationEnabler(myProject, myGit, myPlatformFacade).enable(detectInfo);
}
});
}
@Override
public CheckoutProvider getCheckoutProvider() {
return new GitCheckoutProvider(ServiceManager.getService(Git.class));
}
}