vcs: ignore file generation improvements

* generate file under specific VCS root (IDEA-199969)
* fix generation on some platforms (IDEA-199966)
* generate path with leading and trailing "/"
* project (module) excludes contribution moved to separate ignore provider
This commit is contained in:
Dmitry Zhuravlev
2018-10-08 14:21:28 +03:00
parent 11b8fb508f
commit 5a7f87db36
16 changed files with 150 additions and 79 deletions
@@ -38,6 +38,8 @@
serviceImplementation="com.intellij.openapi.vcs.contentAnnotation.ContentAnnotationCacheImpl"/>
<projectService serviceInterface="com.intellij.openapi.vcs.changes.IgnoredFileGenerator"
serviceImplementation="com.intellij.openapi.vcs.changes.IgnoredFileGeneratorImpl"/>
<projectService serviceInterface="com.intellij.openapi.vcs.changes.IgnoredFileGeneratorImpl$IgnoredFileRootStore"
serviceImplementation="com.intellij.openapi.vcs.changes.IgnoredFileGeneratorImpl$IgnoredFileRootStore"/>
<daemon.intentionActionFilter implementation="com.intellij.vcs.commit.BaseCommitMessageInspection$EmptyIntentionActionFilter"/>
@@ -131,7 +133,8 @@
<checkoutListener implementation="com.intellij.openapi.vcs.checkout.ProjectDirCheckoutListener"/>
<checkoutListener implementation="com.intellij.openapi.vcs.checkout.ProjectImporterCheckoutListener"/>
<ignoredFileProvider implementation="com.intellij.openapi.vcs.changes.ChangeListManagerImpl$DefaultIgnoredFileProvider" order="last"/>
<ignoredFileProvider id="VCS.DefaultIgnoredFileProvider" implementation="com.intellij.openapi.vcs.changes.ChangeListManagerImpl$DefaultIgnoredFileProvider" order="first"/>
<ignoredFileProvider implementation="com.intellij.openapi.vcs.changes.ProjectExcludesIgnoredFileProvider" order="after VCS.DefaultIgnoredFileProvider"/>
</extensions>
</idea-plugin>
@@ -3,6 +3,7 @@ package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.vcs.VcsKey;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
/**
@@ -18,5 +19,5 @@ public interface IgnoredFileContentProvider {
String getFileName();
@NotNull
String buildIgnoreFileContent(@NotNull IgnoredFileProvider[] ignoredFileProviders);
String buildIgnoreFileContent(@NotNull VirtualFile ignoreFileRoot, @NotNull IgnoredFileProvider[] ignoredFileProviders);
}
@@ -2,6 +2,7 @@
package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@@ -13,5 +14,5 @@ import java.io.IOException;
*/
public interface IgnoredFileGenerator {
boolean generateFile(@NotNull AbstractVcs vcs) throws IOException;
boolean generateFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException;
}
@@ -18,6 +18,7 @@ package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
@@ -32,7 +33,7 @@ public interface IgnoredFileProvider {
boolean isIgnoredFile(@NotNull Project project, @NotNull FilePath filePath);
@NotNull
Set<String> getIgnoredFilesMasks(@NotNull Project project);
Set<String> getIgnoredFilesMasks(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot);
@NotNull
String getMasksGroupDescription();
@@ -62,7 +62,6 @@ import javax.swing.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import static com.intellij.openapi.project.Project.DIRECTORY_STORE_FOLDER;
import static com.intellij.openapi.vcs.ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED;
@@ -1536,8 +1535,7 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Projec
@Override
public boolean isIgnoredFile(@NotNull Project project, @NotNull FilePath filePath) {
IProjectStore store = ProjectKt.getStateStore(project);
return getInstanceImpl(project).myIgnoredIdeaLevel.isIgnoredFile(filePath)
|| (!ProjectKt.isDirectoryBased(project) && FileUtilRt.extensionEquals(filePath.getPath(), WorkspaceFileType.DEFAULT_EXTENSION))
return (!ProjectKt.isDirectoryBased(project) && FileUtilRt.extensionEquals(filePath.getPath(), WorkspaceFileType.DEFAULT_EXTENSION))
|| StringsKt.equals(filePath.getPath(), store.getWorkspaceFilePath(), !SystemInfo.isFileSystemCaseSensitive)
|| isShelfDirOrInsideIt(filePath, project);
}
@@ -1549,42 +1547,34 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Projec
@NotNull
@Override
public Set<String> getIgnoredFilesMasks(@NotNull Project project) {
Set<String> masks = ContainerUtil.newHashSet();
masks.addAll(getProjectExcludePathsRelativeTo(project));
@SystemIndependent String projectBasePath = project.getBasePath();
if (projectBasePath != null) {
String relativeShelfPath =
FileUtil.getRelativePath(projectBasePath, ShelveChangesManager.getDefaultShelfPath(project), File.separatorChar);
if (relativeShelfPath != null) {
masks.add(relativeShelfPath);
public Set<String> getIgnoredFilesMasks(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) {
Set<String> masks = ContainerUtil.newLinkedHashSet();
@SystemIndependent String projectBasePath = project.getBasePath();
if (projectBasePath == null) return masks;
if (FileUtil.isAncestor(ignoreFileRoot.getPath(), projectBasePath, false)) {
String defaultShelfPath = ShelveChangesManager.getDefaultShelfPath(project);
if (FileUtil.isAncestor(projectBasePath, defaultShelfPath, true)) {
String relativeShelfPath =
FileUtil.getRelativePath(projectBasePath, defaultShelfPath, '/');
if (relativeShelfPath != null) {
masks.add("/" + relativeShelfPath + "/");
}
}
}
String workspaceFilePath = ProjectKt.getStateStore(project).getWorkspaceFilePath();
if (ProjectKt.isDirectoryBased(project)) {
masks.add(DIRECTORY_STORE_FOLDER + "/workspace.xml");
if (workspaceFilePath != null && FileUtil.isAncestor(ignoreFileRoot.getPath(), workspaceFilePath, false)) {
masks.add("/" + DIRECTORY_STORE_FOLDER + "/workspace.xml");
}
}
else {
masks.add("*." + WorkspaceFileType.DEFAULT_EXTENSION);
}
return ContainerUtil.unmodifiableOrEmptySet(masks);
}
@NotNull
private static Set<String> getProjectExcludePathsRelativeTo(@NotNull Project project) {
Set<String> paths = ContainerUtil.newHashSet();
@SystemIndependent String basePath = project.getBasePath();
assert basePath != null : "Doesn't support default projects";
for (Module module : ModuleManager.getInstance(project).getModules()) {
VirtualFile[] roots = ModuleRootManager.getInstance(module).getExcludeRoots();
paths.addAll(Arrays.stream(roots)
.filter((root) -> FileUtil.isAncestor(basePath, root.getPath(), true))
.map((root) -> FileUtil.getRelativePath(basePath, root.getPath(), File.separatorChar))
.collect(Collectors.toSet()));
}
return paths;
}
@NotNull
@Override
public String getMasksGroupDescription() {
@@ -1,8 +1,8 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vcs.changes;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.notification.NotificationAction;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
@@ -13,23 +13,23 @@ import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.encoding.EncodingProjectManager;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.SystemIndependent;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator {
private static final Logger LOG = Logger.getInstance(IgnoredFileGeneratorImpl.class);
private static final String IGNORE_FILE_GENERATED_PROPERTY = "VCS_IGNOREFILE_GENERATED";
private final Project myProject;
private final Object myWriteLock = new Object();
@@ -39,23 +39,25 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator {
}
@Override
public boolean generateFile(@NotNull AbstractVcs vcs) throws IOException {
if (!needGenerateIgnoreFile(myProject)) {
LOG.debug("Skip VCS ignore file generation");
return false;
}
public boolean generateFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException {
IgnoredFileContentProvider ignoredFileContentProvider = findIgnoredFileContentProvider(vcs);
if (ignoredFileContentProvider == null) {
LOG.debug("Cannot find content provider for vcs " + vcs.getName());
return false;
}
String ignoreFileName = ignoredFileContentProvider.getFileName();
if (!needGenerateIgnoreFile(myProject, ignoreFileRoot)) {
LOG.debug("Skip VCS ignore file generation");
return false;
}
synchronized (myWriteLock) {
File ignoreFile = getIgnoreFile(ignoredFileContentProvider.getFileName());
File ignoreFile = getIgnoreFile(ignoreFileRoot, ignoreFileName);
if (!ignoreFile.exists()) {
String projectCharsetName = EncodingProjectManager.getInstance(myProject).getDefaultCharsetName();
String ignoreFileContent = ignoredFileContentProvider.buildIgnoreFileContent(IgnoredFileProvider.IGNORE_FILE.getExtensions());
String ignoreFileContent = ignoredFileContentProvider.buildIgnoreFileContent(ignoreFileRoot, IgnoredFileProvider.IGNORE_FILE.getExtensions());
FileUtil.writeToFile(ignoreFile, ignoreFileContent.getBytes(projectCharsetName));
LocalFileSystem.getInstance().refreshIoFiles(Collections.singleton(ignoreFile));
notifyAboutIgnoreFileGeneration(ignoreFile);
@@ -74,16 +76,13 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator {
}
@NotNull
private File getIgnoreFile(@NotNull String fileName) {
@SystemIndependent String basePath = myProject.getBasePath();
assert basePath != null : "Doesn't support default projects";
return new File(basePath, fileName);
private static File getIgnoreFile(@NotNull VirtualFile ignoreFileRoot, @NotNull String ignoreFileName) {
File vcsRootFile = VfsUtilCore.virtualToIoFile(ignoreFileRoot);
return new File(vcsRootFile.getPath(), ignoreFileName);
}
private void notifyAboutIgnoreFileGeneration(@NotNull File ignoreFile) {
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(myProject);
propertiesComponent.setValue(IGNORE_FILE_GENERATED_PROPERTY, true);
IgnoredFileRootStore.getInstance(myProject).addRoot(ignoreFile.getParent());
VcsNotifier.getInstance(myProject)
.notifyMinorInfo("",
VcsBundle.message("ignored.file.generation.message", ignoreFile.getName()),
@@ -99,10 +98,47 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator {
}));
}
private static boolean needGenerateIgnoreFile(@NotNull Project project) {
boolean wasGeneratedPreviously = PropertiesComponent.getInstance(project).getBoolean(IGNORE_FILE_GENERATED_PROPERTY, false);
LOG.debug("Ignore file generated previously " + wasGeneratedPreviously);
private static boolean needGenerateIgnoreFile(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) {
boolean wasGeneratedPreviously = IgnoredFileRootStore.getInstance(project).containsRoot(ignoreFileRoot.getPath());
if (wasGeneratedPreviously) {
LOG.debug("Ignore file generated previously for root " + ignoreFileRoot.getPath());
}
boolean needGenerateRegistryFlag = Registry.is("vcs.ignorefile.generation", true);
return !wasGeneratedPreviously && needGenerateRegistryFlag;
}
@State(name = "IgnoredFileRootStore", storages = {@Storage(StoragePathMacros.WORKSPACE_FILE)})
static class IgnoredFileRootStore implements PersistentStateComponent<IgnoredFileRootStore.State> {
static class State {
public Set<String> generatedRoots = ContainerUtil.newHashSet();
}
State myState;
static IgnoredFileRootStore getInstance(Project project) {
return ServiceManager.getService(project, IgnoredFileRootStore.class);
}
boolean containsRoot(@NotNull String root) {
return myState != null && myState.generatedRoots.contains(root);
}
void addRoot(@NotNull String root) {
if (myState != null) {
myState.generatedRoots.add(root);
}
}
@Nullable
@Override
public State getState() {
return myState;
}
@Override
public void loadState(@NotNull State state) {
myState = state;
}
}
}
@@ -0,0 +1,33 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vcs.changes
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.changes.ChangeListManagerImpl.getInstanceImpl
import com.intellij.openapi.vcs.changes.ui.ChangesComparator
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
class ProjectExcludesIgnoredFileProvider : IgnoredFileProvider {
override fun isIgnoredFile(project: Project, filePath: FilePath) = getInstanceImpl(project).ignoredFilesComponent.isIgnoredFile(filePath)
override fun getIgnoredFilesMasks(project: Project, ignoreFileRoot: VirtualFile) = getProjectExcludePathsRelativeTo(project,
ignoreFileRoot)
override fun getMasksGroupDescription() = "Project exclude paths"
private fun getProjectExcludePathsRelativeTo(project: Project, ignoreFileRoot: VirtualFile): Set<String> {
val excludes = sortedSetOf(ChangesComparator.getVirtualFileComparator(false))
for (module in ModuleManager.getInstance(project).modules) {
val roots = ModuleRootManager.getInstance(module).excludeRoots
excludes.addAll(roots.filter { root -> VfsUtilCore.isAncestor(ignoreFileRoot, root, false) })
}
return excludes.map { root -> "/" + FileUtil.getRelativePath(ignoreFileRoot.path, root.path, '/')!! + '/' }.toSet()
}
}
@@ -4,10 +4,10 @@ package com.intellij.openapi.vcs.changes
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.vcsUtil.VcsImplUtil
import com.intellij.vcsUtil.VcsUtil
private val LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.VcsIgnoreFilesChecker")
@@ -16,12 +16,12 @@ class VcsIgnoreFilesChecker : StartupActivity, DumbAware {
override fun runActivity(project: Project) = generateVcsIgnoreFileIfNeeded(project)
private fun generateVcsIgnoreFileIfNeeded(project: Project) {
val basePath = project.basePath
val projectBaseDir = if (basePath != null) LocalFileSystem.getInstance().findFileByPath(basePath) else null
val vcs = ProjectLevelVcsManager.getInstance(project).findVersioningVcs(projectBaseDir)
//at the moment we check and generate if needed ignore file only for projectDir. In future we can utilize VcsRootDetector for that purpose
val projectDir = project.guessProjectDir() ?: return
val vcs = VcsUtil.getVcsFor(project, projectDir)
if (vcs != null) {
LOG.debug("Generate VCS file for $vcs")
VcsImplUtil.generateIgnoreFileIfNeeded(project, vcs)
VcsImplUtil.generateIgnoreFileIfNeeded(project, vcs, projectDir)
}
}
}
@@ -54,7 +54,7 @@ public abstract class VcsIntegrationEnabler {
if (roots.isEmpty()) {
boolean succeeded = initOrNotifyError(projectDir);
if (succeeded) {
VcsImplUtil.generateIgnoreFileIfNeeded(myProject, myVcs);
VcsImplUtil.generateIgnoreFileIfNeeded(myProject, myVcs, projectDir);
addVcsRoots(Collections.singleton(projectDir));
}
}
@@ -55,14 +55,16 @@ public class VcsImplUtil {
return Registry.is("vcs.non.modal.commit");
}
public static boolean generateIgnoreFileIfNeeded(@NotNull Project project, @NotNull AbstractVcs vcs) {
public static boolean generateIgnoreFileIfNeeded(@NotNull Project project,
@NotNull AbstractVcs vcs,
@NotNull VirtualFile ignoreFileRoot) {
IgnoredFileGenerator ignoredFileGenerator = ServiceManager.getService(project, IgnoredFileGenerator.class);
if (ignoredFileGenerator == null) {
LOG.debug("Cannot find ignore file ignoredFileGenerator for " + vcs.getName() + " VCS");
return false;
}
try {
return ignoredFileGenerator.generateFile(vcs);
return ignoredFileGenerator.generateFile(ignoreFileRoot, vcs);
}
catch (IOException e) {
LOG.warn(e);
@@ -352,7 +352,7 @@ public class CloudGitDeploymentRuntime extends CloudDeploymentRuntime {
protected void refreshApplicationRepository() {
Project project = getProject();
GitInit.refreshAndConfigureVcsMappings(project, getRepositoryRoot(), getRepositoryRootFile().getAbsolutePath());
GitUtil.generateGitignoreFileIfNeeded(project);
GitUtil.generateGitignoreFileIfNeeded(project, getRepositoryRoot());
}
protected void pushApplication(@NotNull CloudGitApplication application) throws ServerRuntimeException {
+2 -2
View File
@@ -1049,7 +1049,7 @@ public class GitUtil {
return findRealRepositoryDir(rootDir, pathToDir) != null;
}
public static void generateGitignoreFileIfNeeded(@NotNull Project project){
VcsImplUtil.generateIgnoreFileIfNeeded(project, GitVcs.getInstance(project));
public static void generateGitignoreFileIfNeeded(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) {
VcsImplUtil.generateIgnoreFileIfNeeded(project, GitVcs.getInstance(project), ignoreFileRoot);
}
}
@@ -69,7 +69,7 @@ public class GitInit extends DumbAwareAction {
@Override
public void run(@NotNull ProgressIndicator indicator) {
refreshAndConfigureVcsMappings(project, root, root.getPath());
GitUtil.generateGitignoreFileIfNeeded(project);
GitUtil.generateGitignoreFileIfNeeded(project, root);
}
});
});
@@ -5,6 +5,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.VcsKey
import com.intellij.openapi.vcs.changes.IgnoredFileContentProvider
import com.intellij.openapi.vcs.changes.IgnoredFileProvider
import com.intellij.openapi.vfs.VirtualFile
import git4idea.GitVcs
import git4idea.repo.GitRepositoryFiles.GITIGNORE
import java.lang.System.lineSeparator
@@ -15,19 +16,19 @@ open class GitIgnoredFileContentProvider(private val project: Project) : Ignored
override fun getFileName() = GITIGNORE
override fun buildIgnoreFileContent(ignoredFileProviders: Array<IgnoredFileProvider>): String {
override fun buildIgnoreFileContent(ignoreFileRoot: VirtualFile, ignoredFileProviders: Array<IgnoredFileProvider>): String {
val content = StringBuilder()
for (i in ignoredFileProviders.indices) {
val provider = ignoredFileProviders[i]
val translatedMasks = provider.getIgnoredFilesMasks(project)
if (translatedMasks.isEmpty()) continue
val ignoredFileMasks = provider.getIgnoredFilesMasks(project, ignoreFileRoot)
if (ignoredFileMasks.isEmpty()) continue
val description = provider.masksGroupDescription
if (description.isNotBlank()) {
content.append(prependCommentHashCharacterIfNeeded(description))
content.append(lineSeparator())
}
content.append(translatedMasks.joinToString(lineSeparator()))
content.append(ignoredFileMasks.joinToString(lineSeparator()))
if (i + 1 < ignoredFileProviders.size) {
content.append(lineSeparator()).append(lineSeparator())
@@ -20,7 +20,7 @@ class GitIgnoredFileTest : GitPlatformTest() {
override fun setUp() {
super.setUp()
createRepository(project, projectPath)
GitUtil.generateGitignoreFileIfNeeded(project)
GitUtil.generateGitignoreFileIfNeeded(project, projectRoot)
}
override fun setUpModule() {
@@ -45,17 +45,20 @@ class GitIgnoredFileTest : GitPlatformTest() {
val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
val gitIgnoreExpectedContentList = """
# Default ignored files
/.shelf/
*.iws
.shelf
$OUT
$EXCLUDED
$EXCLUDED_CHILD_DIR
# Project exclude paths
/$EXCLUDED/
/$EXCLUDED_CHILD_DIR/
/$OUT/
""".trimIndent().lines()
val gitIgnoreFile = File("$projectPath/$GITIGNORE")
assertTrue(gitIgnoreFile.exists())
val generatedGitIgnoreContent = gitIgnoreFile.readText(projectCharset)
assertFalse("Generated ignore file is empty", generatedGitIgnoreContent.isBlank())
assertContainsElements(generatedGitIgnoreContent.lines(), gitIgnoreExpectedContentList)
assertFalse("Generated ignore file content should be system-independent", generatedGitIgnoreContent.contains('\\'))
assertContainsOrdered(generatedGitIgnoreContent.lines(), gitIgnoreExpectedContentList)
}
private fun VirtualFile.findOrCreateDir(dirName: String) = this.findChild(dirName) ?: createChildDirectory(this, dirName)
@@ -236,7 +236,7 @@ class GithubShareAction : DumbAwareAction("Share Project on GitHub", "Easily sha
return false
}
GitInit.refreshAndConfigureVcsMappings(project, root, root.path)
GitUtil.generateGitignoreFileIfNeeded(project)
GitUtil.generateGitignoreFileIfNeeded(project, root)
return true
}