/*
* Copyright 2000-2015 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 com.intellij.openapi.vcs.roots;
import com.intellij.idea.ActionsBundle;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static com.intellij.openapi.util.text.StringUtil.pluralize;
/**
* Searches for Vcs roots problems via {@link VcsRootErrorsFinder} and notifies about them.
*/
public class VcsRootProblemNotifier {
public static final Function<VcsRootError, String> PATH_FROM_ROOT_ERROR = VcsRootError::getMapping;
@NotNull private final Project myProject;
@NotNull private final VcsConfiguration mySettings;
@NotNull private final ProjectLevelVcsManager myVcsManager;
@NotNull private final ChangeListManager myChangeListManager;
@NotNull private final ProjectFileIndex myProjectFileIndex;
@NotNull private final Set<String> myReportedUnregisteredRoots;
@Nullable private Notification myNotification;
@NotNull private final Object NOTIFICATION_LOCK = new Object();
public static VcsRootProblemNotifier getInstance(@NotNull Project project) {
return new VcsRootProblemNotifier(project);
}
private VcsRootProblemNotifier(@NotNull Project project) {
myProject = project;
mySettings = VcsConfiguration.getInstance(myProject);
myChangeListManager = ChangeListManager.getInstance(project);
myProjectFileIndex = ProjectFileIndex.SERVICE.getInstance(myProject);
myVcsManager = ProjectLevelVcsManager.getInstance(project);
myReportedUnregisteredRoots = new HashSet<>(mySettings.IGNORED_UNREGISTERED_ROOTS);
}
public void rescanAndNotifyIfNeeded() {
Collection<VcsRootError> errors = scan();
if (errors.isEmpty()) {
synchronized (NOTIFICATION_LOCK) {
expireNotification();
}
return;
}
Collection<VcsRootError> importantUnregisteredRoots = getImportantUnregisteredMappings(errors);
Collection<VcsRootError> invalidRoots = getInvalidRoots(errors);
List<String> unregRootPaths = ContainerUtil.map(importantUnregisteredRoots, PATH_FROM_ROOT_ERROR);
if (invalidRoots.isEmpty() && (importantUnregisteredRoots.isEmpty() || myReportedUnregisteredRoots.containsAll(unregRootPaths))) {
return;
}
myReportedUnregisteredRoots.addAll(unregRootPaths);
String title = makeTitle(importantUnregisteredRoots, invalidRoots);
String description = makeDescription(importantUnregisteredRoots, invalidRoots);
synchronized (NOTIFICATION_LOCK) {
expireNotification();
NotificationListener listener = new MyNotificationListener(myProject, mySettings, myVcsManager, importantUnregisteredRoots);
VcsNotifier notifier = VcsNotifier.getInstance(myProject);
myNotification = invalidRoots.isEmpty()
? notifier.notifyMinorInfo(title, description, listener)
: notifier.notifyError(title, description, listener);
}
}
private boolean isUnderOrAboveProjectDir(@NotNull String mapping) {
String projectDir = ObjectUtils.assertNotNull(myProject.getBasePath());
return mapping.equals(VcsDirectoryMapping.PROJECT_CONSTANT) ||
FileUtil.isAncestor(projectDir, mapping, false) ||
FileUtil.isAncestor(mapping, projectDir, false);
}
private boolean isIgnoredOrExcluded(@NotNull String mapping) {
VirtualFile file = LocalFileSystem.getInstance().findFileByPath(mapping);
return file != null && (myChangeListManager.isIgnoredFile(file) || myProjectFileIndex.isExcluded(file));
}
private void expireNotification() {
if (myNotification != null) {
final Notification notification = myNotification;
ApplicationManager.getApplication().invokeLater(notification::expire);
myNotification = null;
}
}
@NotNull
private Collection<VcsRootError> scan() {
return new VcsRootErrorsFinder(myProject).find();
}
@SuppressWarnings("StringConcatenationInsideStringBufferAppend")
@NotNull
private static String makeDescription(@NotNull Collection<VcsRootError> unregisteredRoots,
@NotNull Collection<VcsRootError> invalidRoots) {
Function<VcsRootError, String> rootToDisplayableString = rootError -> {
if (rootError.getMapping().equals(VcsDirectoryMapping.PROJECT_CONSTANT)) {
return StringUtil.escapeXml(rootError.getMapping());
}
return FileUtil.toSystemDependentName(rootError.getMapping());
};
StringBuilder description = new StringBuilder();
if (!invalidRoots.isEmpty()) {
if (invalidRoots.size() == 1) {
VcsRootError rootError = invalidRoots.iterator().next();
String vcsName = rootError.getVcsKey().getName();
description.append(String.format("The directory %s is registered as a %s root, but no %s repositories were found there.",
rootToDisplayableString.fun(rootError), vcsName, vcsName));
}
else {
description.append("The following directories are registered as VCS roots, but they are not: <br/>" +
StringUtil.join(invalidRoots, rootToDisplayableString, "<br/>"));
}
description.append("<br/>");
}
if (!unregisteredRoots.isEmpty()) {
if (unregisteredRoots.size() == 1) {
VcsRootError unregisteredRoot = unregisteredRoots.iterator().next();
description.append(String.format("The directory %s is under %s, but is not registered in the Settings.",
rootToDisplayableString.fun(unregisteredRoot), unregisteredRoot.getVcsKey().getName()));
}
else {
description.append("The following directories are roots of VCS repositories, but they are not registered in the Settings: <br/>" +
StringUtil.join(unregisteredRoots, rootToDisplayableString, "<br/>"));
}
description.append("<br/>");
}
String add = invalidRoots.isEmpty() ? "<a href='add'>Add " + pluralize("root", unregisteredRoots.size()) + "</a> " : "";
String configure = "<a href='configure'>Configure</a>";
String ignore = invalidRoots.isEmpty() ? " <a href='ignore'>Ignore</a>" : "";
description.append(add + configure + ignore);
return description.toString();
}
@NotNull
private static String makeTitle(@NotNull Collection<VcsRootError> unregisteredRoots, @NotNull Collection<VcsRootError> invalidRoots) {
String title;
if (unregisteredRoots.isEmpty()) {
title = "Invalid VCS root " + pluralize("mapping", invalidRoots.size());
}
else if (invalidRoots.isEmpty()) {
title = "Unregistered VCS " + pluralize("root", unregisteredRoots.size()) + " detected";
}
else {
title = "VCS root configuration problems";
}
return title;
}
@NotNull
private List<VcsRootError> getImportantUnregisteredMappings(@NotNull Collection<VcsRootError> errors) {
return ContainerUtil.filter(errors, error -> {
String mapping = error.getMapping();
return error.getType() == VcsRootError.Type.UNREGISTERED_ROOT && isUnderOrAboveProjectDir(mapping) && !isIgnoredOrExcluded(mapping);
});
}
@NotNull
private static Collection<VcsRootError> getInvalidRoots(@NotNull Collection<VcsRootError> errors) {
return ContainerUtil.filter(errors, error -> error.getType() == VcsRootError.Type.EXTRA_MAPPING);
}
private static class MyNotificationListener extends NotificationListener.Adapter {
@NotNull private final Project myProject;
@NotNull private final VcsConfiguration mySettings;
@NotNull private final ProjectLevelVcsManager myVcsManager;
@NotNull private final Collection<VcsRootError> myImportantUnregisteredRoots;
private MyNotificationListener(@NotNull Project project,
@NotNull VcsConfiguration settings,
@NotNull ProjectLevelVcsManager vcsManager,
@NotNull Collection<VcsRootError> importantUnregisteredRoots) {
myProject = project;
mySettings = settings;
myVcsManager = vcsManager;
myImportantUnregisteredRoots = importantUnregisteredRoots;
}
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getDescription().equals("configure") && !myProject.isDisposed()) {
ShowSettingsUtil.getInstance().showSettingsDialog(myProject, ActionsBundle.message("group.VcsGroup.text"));
Collection<VcsRootError> errorsAfterPossibleFix = getInstance(myProject).scan();
if (errorsAfterPossibleFix.isEmpty() && !notification.isExpired()) {
notification.expire();
}
}
else if (event.getDescription().equals("ignore")) {
mySettings.addIgnoredUnregisteredRoots(ContainerUtil.map(myImportantUnregisteredRoots, PATH_FROM_ROOT_ERROR));
notification.expire();
}
else if (event.getDescription().equals("add")) {
List<VcsDirectoryMapping> mappings = myVcsManager.getDirectoryMappings();
for (VcsRootError root : myImportantUnregisteredRoots) {
mappings = VcsUtil.addMapping(mappings, root.getMapping(), root.getVcsKey().getName());
}
myVcsManager.setDirectoryMappings(mappings);
}
}
}
}