diff --git a/platform/platform-resources-en/src/messages/VcsBundle.properties b/platform/platform-resources-en/src/messages/VcsBundle.properties index 162108321bd2..406519d618c6 100644 --- a/platform/platform-resources-en/src/messages/VcsBundle.properties +++ b/platform/platform-resources-en/src/messages/VcsBundle.properties @@ -380,8 +380,10 @@ ignored.edit.multiple.files=Selected {0} files ignored.edit.radio.file=Ignore specified &file ignored.edit.radio.directory=Ignore all files &under ignored.edit.radio.mask=Ignore all files &matching -ignored.file.generation.message={0} file created -ignored.file.generation.review=Open +ignored.file.manage.message=Manage VCS ignore files automatically +ignored.file.manage.this.project=For this project +ignored.file.manage.all.project=For all projects +ignored.file.manage.notnow=Not now browse.changes.content.title=Changes under {0} browse.changes.no.filter.prompt=You have not specified any filtering criteria. Are you sure you would like to view the entire history of the project? browse.changes.title=Browse Changes diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index c2a5b1e831b4..2d9b49578b51 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -896,7 +896,7 @@ cvs.roots.refresh.uses.vfs.description=Should CVS roots refresh after update use cvs.server.output.max.lines=1000 cvs.server.output.max.lines.description=The maximum number of lines that are displayed per command in the CVS console vcs.add.remove.silent=true -vcs.ignorefile.generation=false +vcs.ignorefile.generation=true vcs.ignorefile.generation.description=If true, native VCS ignored files (e.g. .gitignore) are generated on first project open (import) and after creating a VCS repository from IDEA. ide.ui.scale.override=false diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsApplicationSettings.java b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsApplicationSettings.java index 916a4a31b497..1f490d7c553d 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsApplicationSettings.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsApplicationSettings.java @@ -22,6 +22,7 @@ public class VcsApplicationSettings implements PersistentStateComponent getIgnoredFilesMasks(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot); + Set getIgnoredFiles(@NotNull Project project); @NotNull - String getMasksGroupDescription(); + String getIgnoredGroupDescription(); } \ No newline at end of file diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ChangeListManagerImpl.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ChangeListManagerImpl.java index 17188b527557..4597dd7c121a 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ChangeListManagerImpl.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ChangeListManagerImpl.java @@ -1548,41 +1548,23 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Projec @NotNull @Override - public Set getIgnoredFilesMasks(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) { - Set masks = ContainerUtil.newLinkedHashSet(); - @SystemIndependent String projectBasePath = project.getBasePath(); - if (projectBasePath == null) return masks; - if (!FileUtil.isAncestor(ignoreFileRoot.getPath(), projectBasePath, false)) return masks; + public Set getIgnoredFiles(@NotNull Project project) { + Set ignored = ContainerUtil.newLinkedHashSet(); String shelfPath = ShelveChangesManager.getShelfPath(project); - if (FileUtil.isAncestor(ignoreFileRoot.getPath(), shelfPath, false)) { - String relativeShelfPath = - FileUtil.getRelativePath(ignoreFileRoot.getPath(), shelfPath, '/'); - if (relativeShelfPath != null) { - masks.add("/" + relativeShelfPath + "/"); - } + ignored.add(IgnoredBeanFactory.ignoreUnderDirectory(shelfPath, project)); + + String workspaceFilePath = ProjectKt.getStateStore(project).getWorkspaceFilePath(); + if (workspaceFilePath != null) { + ignored.add(IgnoredBeanFactory.ignoreFile(workspaceFilePath, project)); } - if (ProjectKt.isDirectoryBased(project)) { - String workspaceFilePath = ProjectKt.getStateStore(project).getWorkspaceFilePath(); - if (workspaceFilePath != null && FileUtil.isAncestor(ignoreFileRoot.getPath(), workspaceFilePath, false)) { - String relativeWorkspaceFilePath = - FileUtil.getRelativePath(ignoreFileRoot.getPath(), workspaceFilePath, '/'); - if (relativeWorkspaceFilePath != null) { - masks.add("/" + relativeWorkspaceFilePath); - } - } - } - else { - masks.add("*." + WorkspaceFileType.DEFAULT_EXTENSION); - } - - return ContainerUtil.unmodifiableOrEmptySet(masks); + return ContainerUtil.unmodifiableOrEmptySet(ignored); } @NotNull @Override - public String getMasksGroupDescription() { + public String getIgnoredGroupDescription() { return "Default ignored files"; } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/IgnoredFileGeneratorImpl.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/IgnoredFileGeneratorImpl.java index cc650fe535f0..dc59a770ac03 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/IgnoredFileGeneratorImpl.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/IgnoredFileGeneratorImpl.java @@ -1,18 +1,14 @@ // 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.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; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcs; -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; @@ -40,6 +36,16 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator { @Override public boolean generateFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException { + return doGenerateOrAppend(ignoreFileRoot, vcs, false); + } + + @Override + public boolean appendFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException { + return doGenerateOrAppend(ignoreFileRoot, vcs, true); + } + + private boolean doGenerateOrAppend(@NotNull VirtualFile ignoreFileRoot, + @NotNull AbstractVcs vcs, boolean append) throws IOException { IgnoredFileContentProvider ignoredFileContentProvider = findIgnoredFileContentProvider(vcs); if (ignoredFileContentProvider == null) { LOG.debug("Cannot find content provider for vcs " + vcs.getName()); @@ -54,16 +60,18 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator { } synchronized (myWriteLock) { + String ignoreFileContent = + ignoredFileContentProvider.buildIgnoreFileContent(ignoreFileRoot, IgnoredFileProvider.IGNORE_FILE.getExtensions()); + if (StringUtil.isEmptyOrSpaces(ignoreFileContent)) return false; + File ignoreFile = getIgnoreFile(ignoreFileRoot, ignoreFileName); - if (!ignoreFile.exists()) { - String projectCharsetName = EncodingProjectManager.getInstance(myProject).getDefaultCharsetName(); - String ignoreFileContent = ignoredFileContentProvider.buildIgnoreFileContent(ignoreFileRoot, IgnoredFileProvider.IGNORE_FILE.getExtensions()); - FileUtil.writeToFile(ignoreFile, ignoreFileContent.getBytes(projectCharsetName)); - LocalFileSystem.getInstance().refreshIoFiles(Collections.singleton(ignoreFile)); - notifyAboutIgnoreFileGeneration(ignoreFile); - return true; - } - return false; + if (ignoreFile.exists() && !append) return false; + + String projectCharsetName = EncodingProjectManager.getInstance(myProject).getDefaultCharsetName(); + FileUtil.writeToFile(ignoreFile, ignoreFileContent.getBytes(projectCharsetName), append); + IgnoredFileRootStore.getInstance(myProject).addRoot(ignoreFile.getParent()); + LocalFileSystem.getInstance().refreshIoFiles(Collections.singleton(ignoreFile)); + return true; } } @@ -81,23 +89,6 @@ public class IgnoredFileGeneratorImpl implements IgnoredFileGenerator { return new File(vcsRootFile.getPath(), ignoreFileName); } - private void notifyAboutIgnoreFileGeneration(@NotNull File ignoreFile) { - IgnoredFileRootStore.getInstance(myProject).addRoot(ignoreFile.getParent()); - VcsNotifier.getInstance(myProject) - .notifyMinorInfo("", - VcsBundle.message("ignored.file.generation.message", ignoreFile.getName()), - NotificationAction.create(VcsBundle.message("ignored.file.generation.review"), (event, notification) -> { - notification.expire(); - VirtualFile ignoreVirtualFile = VfsUtil.findFileByIoFile(ignoreFile, true); - if (ignoreVirtualFile != null) { - new OpenFileDescriptor(myProject, ignoreVirtualFile).navigate(true); - } - else { - LOG.warn("Cannot find ignore file " + ignoreFile.getName()); - } - })); - } - private static boolean needGenerateIgnoreFile(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) { boolean wasGeneratedPreviously = IgnoredFileRootStore.getInstance(project).containsRoot(ignoreFileRoot.getPath()); if (wasGeneratedPreviously) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ProjectExcludesIgnoredFileProvider.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ProjectExcludesIgnoredFileProvider.kt index 0488e7cd2c8f..bd5840099609 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ProjectExcludesIgnoredFileProvider.kt +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ProjectExcludesIgnoredFileProvider.kt @@ -4,30 +4,28 @@ 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 getIgnoredFiles(project: Project) = getProjectExcludePathsRelativeTo(project) - override fun getMasksGroupDescription() = "Project exclude paths" + override fun getIgnoredGroupDescription() = "Project exclude paths" - private fun getProjectExcludePathsRelativeTo(project: Project, ignoreFileRoot: VirtualFile): Set { + private fun getProjectExcludePathsRelativeTo(project: Project): Set { val excludes = sortedSetOf(ChangesComparator.getVirtualFileComparator(false)) for (module in ModuleManager.getInstance(project).modules) { + if(module.isDisposed) continue + val roots = ModuleRootManager.getInstance(module).excludeRoots - excludes.addAll(roots.filter { root -> VfsUtilCore.isAncestor(ignoreFileRoot, root, false) }) + excludes.addAll(roots) } - return excludes.map { root -> "/" + FileUtil.getRelativePath(ignoreFileRoot.path, root.path, '/')!! + '/' }.toSet() + return excludes.map { root -> IgnoredBeanFactory.ignoreUnderDirectory(root.path, project) }.toSet() } } \ No newline at end of file diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/VcsIgnoreFilesChecker.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/VcsIgnoreFilesChecker.kt index 35cc87d69159..12c42c877441 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/VcsIgnoreFilesChecker.kt +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/VcsIgnoreFilesChecker.kt @@ -3,30 +3,40 @@ package com.intellij.openapi.vcs.changes import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.ProjectComponent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED import com.intellij.openapi.vcs.VcsListener +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.project.isDirectoryBased +import com.intellij.project.stateStore import com.intellij.vcsUtil.VcsImplUtil import com.intellij.vcsUtil.VcsUtil +private val LOG = Logger.getInstance(VcsIgnoreFilesChecker::class.java) + class VcsIgnoreFilesChecker(private val project: Project) : ProjectComponent { - override fun projectOpened() = - project.messageBus - .connect() - .subscribe(VCS_CONFIGURATION_CHANGED, VcsListener { - generateVcsIgnoreFileIfNeeded(project) - }) + override fun projectOpened() { + if (project.isDirectoryBased && !ApplicationManager.getApplication().isUnitTestMode) { + project.messageBus + .connect() + .subscribe(VCS_CONFIGURATION_CHANGED, VcsListener { + generateVcsIgnoreFileIfNeeded(project) + }) + } + } private fun generateVcsIgnoreFileIfNeeded(project: Project) = ApplicationManager.getApplication().executeOnPooledThread { - if (!project.isDisposed) { - val projectFile = project.projectFile ?: return@executeOnPooledThread + if (project.isDisposed) return@executeOnPooledThread - val projectVcsRoot = VcsUtil.getVcsRootFor(project, projectFile) - if (projectVcsRoot != null) { - VcsImplUtil.generateIgnoreFileIfNeeded(project, projectVcsRoot) - } - } + val projectConfigDirPath = project.stateStore.projectConfigDir ?: return@executeOnPooledThread + val projectConfigDirVFile = LocalFileSystem.getInstance().findFileByPath(projectConfigDirPath) ?: return@executeOnPooledThread + + val vcs = VcsUtil.getVcsFor(project, projectConfigDirVFile) ?: return@executeOnPooledThread + + LOG.debug("Generate VCS ignore file for " + vcs.name) + VcsImplUtil.generateIgnoreFileIfNeeded(project, vcs, projectConfigDirVFile) } } \ No newline at end of file diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.form b/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.form index d3aa04685829..dcb27634084f 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.form +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.form @@ -148,7 +148,7 @@ - + @@ -166,7 +166,7 @@ - + @@ -198,6 +198,14 @@ + + + + + + + + diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.java index 1426265d2092..cbf8471e748c 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/configurable/VcsGeneralConfigurationPanel.java @@ -62,6 +62,7 @@ public class VcsGeneralConfigurationPanel { private JCheckBox myReloadContext; private JLabel myOnPatchCreationLabel; private JPanel myEmptyChangeListPanel; + private JCheckBox myManageIgnoreFiles; private ButtonGroup myEmptyChangelistRemovingGroup; public VcsGeneralConfigurationPanel(final Project project) { @@ -102,9 +103,11 @@ public class VcsGeneralConfigurationPanel { public void apply() { VcsConfiguration settings = VcsConfiguration.getInstance(myProject); + VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance(); settings.REMOVE_EMPTY_INACTIVE_CHANGELISTS = getSelected(myEmptyChangelistRemovingGroup); settings.RELOAD_CONTEXT = myReloadContext.isSelected(); + applicationSettings.MANAGE_IGNORE_FILES = myManageIgnoreFiles.isSelected(); for (VcsShowOptionsSettingImpl setting : myPromptOptions.keySet()) { setting.setValue(myPromptOptions.get(setting).isSelected()); @@ -186,10 +189,12 @@ public class VcsGeneralConfigurationPanel { public boolean isModified() { VcsConfiguration settings = VcsConfiguration.getInstance(myProject); + VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance(); if (settings.REMOVE_EMPTY_INACTIVE_CHANGELISTS != getSelected(myEmptyChangelistRemovingGroup)){ return true; } if (settings.RELOAD_CONTEXT != myReloadContext.isSelected()) return true; + if (applicationSettings.MANAGE_IGNORE_FILES != myManageIgnoreFiles.isSelected()) return true; if (getReadOnlyStatusHandler().getState().SHOW_DIALOG != myShowReadOnlyStatusDialog.isSelected()) { return true; @@ -208,7 +213,9 @@ public class VcsGeneralConfigurationPanel { public void reset() { VcsConfiguration settings = VcsConfiguration.getInstance(myProject); + VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance(); myReloadContext.setSelected(settings.RELOAD_CONTEXT); + myManageIgnoreFiles.setSelected(applicationSettings.MANAGE_IGNORE_FILES); VcsShowConfirmationOption.Value value = settings.REMOVE_EMPTY_INACTIVE_CHANGELISTS; UIUtil.setSelectedButton(myEmptyChangelistRemovingGroup, value == VcsShowConfirmationOption.Value.SHOW_CONFIRMATION ? 0 diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/roots/VcsIntegrationEnabler.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/roots/VcsIntegrationEnabler.java index a57ede553a97..f3262fd9e871 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/roots/VcsIntegrationEnabler.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/roots/VcsIntegrationEnabler.java @@ -54,7 +54,7 @@ public abstract class VcsIntegrationEnabler { if (roots.isEmpty()) { boolean succeeded = initOrNotifyError(projectDir); if (succeeded) { - VcsImplUtil.generateIgnoreFileIfNeeded(myProject, myVcs, projectDir); + VcsImplUtil.proposeUpdateIgnoreFile(myProject, myVcs, projectDir); addVcsRoots(Collections.singleton(projectDir)); } } diff --git a/platform/vcs-impl/src/com/intellij/vcsUtil/VcsImplUtil.java b/platform/vcs-impl/src/com/intellij/vcsUtil/VcsImplUtil.java index 4e2208d842de..1daaa8bb93c3 100644 --- a/platform/vcs-impl/src/com/intellij/vcsUtil/VcsImplUtil.java +++ b/platform/vcs-impl/src/com/intellij/vcsUtil/VcsImplUtil.java @@ -1,13 +1,21 @@ // 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.vcsUtil; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.notification.NotificationAction; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vcs.AbstractVcs; +import com.intellij.openapi.vcs.VcsApplicationSettings; +import com.intellij.openapi.vcs.VcsBundle; +import com.intellij.openapi.vcs.VcsNotifier; +import com.intellij.openapi.vcs.changes.IgnoredFileContentProvider; import com.intellij.openapi.vcs.changes.IgnoredFileGenerator; +import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.WaitForProgressToShow; @@ -15,6 +23,7 @@ import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; /** *

{@link VcsUtil} extension that needs access to the {@code intellij.platform.vcs.impl} module.

@@ -23,6 +32,8 @@ public class VcsImplUtil { private static final Logger LOG = Logger.getInstance(VcsImplUtil.class); + public static final String MANAGE_IGNORE_FILES_PROPERTY = "MANAGE_IGNORE_FILES_PROPERTY"; + /** * Shows error message with specified message text and title. * The parent component is the root frame. @@ -55,31 +66,95 @@ public class VcsImplUtil { return Registry.is("vcs.non.modal.commit"); } - public static void generateIgnoreFileIfNeeded(@NotNull Project project, @NotNull VirtualFile vcsRoot) { - AbstractVcs vcs = VcsUtil.getVcsFor(project, vcsRoot); - if (vcs == null) { - LOG.debug("Cannot get VCS for root " + vcsRoot.getPath()); - return; - } + public static void proposeUpdateIgnoreFile(@NotNull Project project, + @NotNull AbstractVcs vcs, + @NotNull VirtualFile ignoreFileRoot) { + IgnoredFileContentProvider ignoreContentProvider = getIgnoredFileContentProvider(project, vcs); - LOG.debug("Generate VCS ignore file for " + vcs.getName()); - generateIgnoreFileIfNeeded(project, vcs, vcsRoot); + if (ignoreContentProvider == null) { + LOG.debug("Cannot get ignore content provider for vcs " + vcs.getName()); + return; + } + + String ignoreFileName = ignoreContentProvider.getFileName(); + File ignoreFile = Paths.get(ignoreFileRoot.getPath(), ignoreFileName).toFile(); + + if (canManageIgnoreFiles(project)) { + updateIgnoreFileIfNeeded(project, vcs, ignoreFileRoot, ignoreFile.exists()); + } + else { + notifyVcsIgnoreFileManage(project, () -> updateIgnoreFileAndOpen(project, vcs, ignoreFileRoot, ignoreFile)); + } + } + + private static void notifyVcsIgnoreFileManage(@NotNull Project project, + @NotNull Runnable manageIgnore) { + PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project); + VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance(); + + VcsNotifier.getInstance(project).notifyMinorInfo( + "", + VcsBundle.message("ignored.file.manage.message"), + NotificationAction.create(VcsBundle.message("ignored.file.manage.this.project"), (event, notification) -> { + manageIgnore.run(); + propertiesComponent.setValue(MANAGE_IGNORE_FILES_PROPERTY, true); + notification.expire(); + }), + NotificationAction.create(VcsBundle.message("ignored.file.manage.all.project"), (event, notification) -> { + manageIgnore.run(); + applicationSettings.MANAGE_IGNORE_FILES = true; + notification.expire(); + }), + NotificationAction.create(VcsBundle.message("ignored.file.manage.notnow"), (event, notification) -> { + notification.expire(); + })); } public static boolean generateIgnoreFileIfNeeded(@NotNull Project project, @NotNull AbstractVcs vcs, @NotNull VirtualFile ignoreFileRoot) { + return updateIgnoreFileIfNeeded(project, vcs, ignoreFileRoot, false); + } + + public static boolean updateIgnoreFileIfNeeded(@NotNull Project project, + @NotNull AbstractVcs vcs, + @NotNull VirtualFile ignoreFileRoot, boolean append) { 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(ignoreFileRoot, vcs); + return append ? ignoredFileGenerator.appendFile(ignoreFileRoot, vcs) : ignoredFileGenerator.generateFile(ignoreFileRoot, vcs); } catch (IOException e) { LOG.warn(e); return false; } } -} + + private static void updateIgnoreFileAndOpen(@NotNull Project project, + @NotNull AbstractVcs vcs, + @NotNull VirtualFile ignoreFileRoot, @NotNull File ignoreFile) { + if (updateIgnoreFileIfNeeded(project, vcs, ignoreFileRoot, ignoreFile.exists())) { + VirtualFile ignoreVFile = VfsUtil.findFileByIoFile(ignoreFile, true); + if (ignoreVFile == null) return; + new OpenFileDescriptor(project, ignoreVFile).navigate(true); + } + } + + private static IgnoredFileContentProvider getIgnoredFileContentProvider(@NotNull Project project, + @NotNull AbstractVcs vcs) { + return IgnoredFileContentProvider.IGNORE_FILE_CONTENT_PROVIDER.extensions(project) + .filter((provider) -> provider.getSupportedVcs().equals(vcs.getKeyInstanceMethod())) + .findFirst() + .orElse(null); + } + + public static boolean canManageIgnoreFiles(@NotNull Project project) { + PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project); + VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance(); + + return applicationSettings.MANAGE_IGNORE_FILES || propertiesComponent.getBoolean(MANAGE_IGNORE_FILES_PROPERTY, false); + } +} \ No newline at end of file diff --git a/plugins/git4idea/remote-servers-git/src/com/intellij/remoteServer/util/CloudGitDeploymentRuntime.java b/plugins/git4idea/remote-servers-git/src/com/intellij/remoteServer/util/CloudGitDeploymentRuntime.java index 32523f88c6d7..a5fb4fa29c59 100644 --- a/plugins/git4idea/remote-servers-git/src/com/intellij/remoteServer/util/CloudGitDeploymentRuntime.java +++ b/plugins/git4idea/remote-servers-git/src/com/intellij/remoteServer/util/CloudGitDeploymentRuntime.java @@ -352,7 +352,7 @@ public class CloudGitDeploymentRuntime extends CloudDeploymentRuntime { protected void refreshApplicationRepository() { Project project = getProject(); GitInit.refreshAndConfigureVcsMappings(project, getRepositoryRoot(), getRepositoryRootFile().getAbsolutePath()); - GitUtil.generateGitignoreFileIfNeeded(project, getRepositoryRoot()); + GitUtil.proposeUpdateGitignore(project, getRepositoryRoot()); } protected void pushApplication(@NotNull CloudGitApplication application) throws ServerRuntimeException { diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index 546bf21b6dfe..5062d84973e3 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -1052,4 +1052,8 @@ public class GitUtil { public static void generateGitignoreFileIfNeeded(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) { VcsImplUtil.generateIgnoreFileIfNeeded(project, GitVcs.getInstance(project), ignoreFileRoot); } + + public static void proposeUpdateGitignore(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) { + VcsImplUtil.proposeUpdateIgnoreFile(project, GitVcs.getInstance(project), ignoreFileRoot); + } } diff --git a/plugins/git4idea/src/git4idea/actions/GitInit.java b/plugins/git4idea/src/git4idea/actions/GitInit.java index 6575b819140a..bf84c30ae4b4 100644 --- a/plugins/git4idea/src/git4idea/actions/GitInit.java +++ b/plugins/git4idea/src/git4idea/actions/GitInit.java @@ -69,7 +69,7 @@ public class GitInit extends DumbAwareAction { @Override public void run(@NotNull ProgressIndicator indicator) { refreshAndConfigureVcsMappings(project, root, root.getPath()); - GitUtil.generateGitignoreFileIfNeeded(project, root); + GitUtil.proposeUpdateGitignore(project, root); } }); }); diff --git a/plugins/git4idea/src/git4idea/ignore/GitIgnoredFileContentProvider.kt b/plugins/git4idea/src/git4idea/ignore/GitIgnoredFileContentProvider.kt index e1e7296c0fa4..d69361528be5 100644 --- a/plugins/git4idea/src/git4idea/ignore/GitIgnoredFileContentProvider.kt +++ b/plugins/git4idea/src/git4idea/ignore/GitIgnoredFileContentProvider.kt @@ -2,11 +2,21 @@ package git4idea.ignore import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Comparing +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vcs.VcsKey +import com.intellij.openapi.vcs.actions.VcsContextFactory +import com.intellij.openapi.vcs.changes.IgnoreSettingsType.* import com.intellij.openapi.vcs.changes.IgnoredFileContentProvider +import com.intellij.openapi.vcs.changes.IgnoredFileDescriptor import com.intellij.openapi.vcs.changes.IgnoredFileProvider +import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.project.stateStore +import com.intellij.vcsUtil.VcsUtil +import git4idea.GitUtil import git4idea.GitVcs +import git4idea.commands.Git import git4idea.repo.GitRepositoryFiles.GITIGNORE import java.lang.System.lineSeparator @@ -17,28 +27,83 @@ open class GitIgnoredFileContentProvider(private val project: Project) : Ignored override fun getFileName() = GITIGNORE override fun buildIgnoreFileContent(ignoreFileRoot: VirtualFile, ignoredFileProviders: Array): String { + if (!GitUtil.isUnderGit(ignoreFileRoot)) return "" //if ignore file root not under git --> return (e.g. in case if .git folder was deleted externally) + val content = StringBuilder() val lineSeparator = lineSeparator() + val untrackedFiles = Git.getInstance().untrackedFiles(project, ignoreFileRoot, null) for (i in ignoredFileProviders.indices) { val provider = ignoredFileProviders[i] - val ignoredFileMasks = provider.getIgnoredFilesMasks(project, ignoreFileRoot) - if (ignoredFileMasks.isEmpty()) continue + val ignoredFiles = provider.getIgnoredFiles(project).ignoreBeansToRelativePaths(ignoreFileRoot, untrackedFiles) + + if (ignoredFiles.isEmpty()) continue if (!content.isEmpty()) { content.append(lineSeparator).append(lineSeparator) } - val description = provider.masksGroupDescription + val description = provider.ignoredGroupDescription if (description.isNotBlank()) { content.append(prependCommentHashCharacterIfNeeded(description)) content.append(lineSeparator) } - content.append(ignoredFileMasks.joinToString(lineSeparator)) + content.append(ignoredFiles.joinToString(lineSeparator)) } return content.toString() } + private fun Iterable.ignoreBeansToRelativePaths(ignoreFileRoot: VirtualFile, untrackedFiles: Set): List { + val vcsRoot= VcsUtil.getVcsRootFor(project, ignoreFileRoot) + val vcsContextFactory = VcsContextFactory.SERVICE.getInstance() + return filter { ignoredBean -> + when (ignoredBean.type) { + UNDER_DIR -> shouldIgnoreUnderDir(ignoredBean, ignoreFileRoot, vcsRoot, vcsContextFactory) + FILE -> shouldIgnoreFile(ignoredBean, untrackedFiles, ignoreFileRoot, vcsRoot, vcsContextFactory) + MASK -> shouldIgnoreByMask(ignoredBean, untrackedFiles) + } + }.map { ignoredBean -> + when (ignoredBean.type) { + MASK -> ignoredBean.mask!! + UNDER_DIR -> "/${FileUtil.getRelativePath(ignoreFileRoot.path, ignoredBean.path!!, '/')!!}" + FILE -> "/${FileUtil.getRelativePath(ignoreFileRoot.path, ignoredBean.path!!, '/')!!}" + } + } + } + + private fun shouldIgnoreUnderDir(ignoredBean: IgnoredFileDescriptor, + ignoreFileRoot: VirtualFile, + vcsRoot: VirtualFile?, + vcsContextFactory: VcsContextFactory) = + FileUtil.exists(ignoredBean.path) + && FileUtil.isAncestor(ignoreFileRoot.path, ignoredBean.path!!, false) + && Comparing.equal(vcsRoot, VcsUtil.getVcsRootFor(project, vcsContextFactory.createFilePath(ignoredBean.path!!, true))) + && shouldNotConsiderInternalIgnoreFile(ignoredBean, ignoreFileRoot) + + private fun shouldIgnoreFile(ignoredBean: IgnoredFileDescriptor, + untrackedFiles: Set, + ignoreFileRoot: VirtualFile, + vcsRoot: VirtualFile?, + vcsContextFactory: VcsContextFactory) = + FileUtil.exists(ignoredBean.path) + && untrackedFiles.any { ignoredBean.matchesFile(it) } + && FileUtil.isAncestor(ignoreFileRoot.path, ignoredBean.path!!, false) + && Comparing.equal(vcsRoot, VcsUtil.getVcsRootFor(project, vcsContextFactory.createFilePath(ignoredBean.path!!, false))) + && shouldNotConsiderInternalIgnoreFile(ignoredBean, ignoreFileRoot) + + private fun shouldIgnoreByMask(ignoredBean: IgnoredFileDescriptor, untrackedFiles: Set) = + untrackedFiles.any { ignoredBean.matchesFile(it) } + + private fun shouldNotConsiderInternalIgnoreFile(ignoredBean: IgnoredFileDescriptor, ignoreFileRoot: VirtualFile): Boolean { + val insideDirectoryStore = ignoredBean.path?.contains(Project.DIRECTORY_STORE_FOLDER) ?: false + if (insideDirectoryStore) { + val directoryStoreOrProjectFileLocation = project.stateStore.directoryStoreFile ?: project.projectFile?.parent ?: return false + return FileUtil.isAncestor(VfsUtilCore.virtualToIoFile(directoryStoreOrProjectFileLocation), + VfsUtilCore.virtualToIoFile(ignoreFileRoot), false) + } + return true + } + private fun prependCommentHashCharacterIfNeeded(description: String): String = if (description.startsWith("#")) description else "# $description" } diff --git a/plugins/git4idea/tests/git4idea/ignore/GitIgnoredFileTest.kt b/plugins/git4idea/tests/git4idea/ignore/GitIgnoredFileTest.kt index 06e39b04d9aa..5798c55b47dd 100644 --- a/plugins/git4idea/tests/git4idea/ignore/GitIgnoredFileTest.kt +++ b/plugins/git4idea/tests/git4idea/ignore/GitIgnoredFileTest.kt @@ -2,27 +2,42 @@ package git4idea.ignore import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.project.Project.DIRECTORY_STORE_FOLDER +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager +import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.encoding.EncodingProjectManager +import com.intellij.project.stateStore import git4idea.GitUtil import git4idea.repo.GitRepositoryFiles.GITIGNORE import git4idea.test.GitPlatformTest import git4idea.test.createRepository import java.io.File +import java.nio.file.Path +import java.nio.file.Paths const val OUT = "out" const val EXCLUDED = "excluded" -const val EXCLUDED_CHILD = "child" -const val EXCLUDED_CHILD_DIR = "$EXCLUDED/$EXCLUDED_CHILD" +const val EXCLUDED_CHILD_DIR = "child" +const val EXCLUDED_CHILD = "$EXCLUDED/$EXCLUDED_CHILD_DIR" +const val SHELF = "shelf" class GitIgnoredFileTest : GitPlatformTest() { + override fun getProjectDirOrFile(): Path { + val projectRoot = File(testRoot, "project") + val file: File = FileUtil.createTempDirectory(projectRoot, FileUtil.sanitizeFileName(name, true), "") + val ideaDir = file.resolve(DIRECTORY_STORE_FOLDER) + ideaDir.mkdir() + return file.toPath() + } + override fun setUp() { super.setUp() Registry.get("vcs.ignorefile.generation").setValue(true, testRootDisposable) createRepository(project, projectPath) - GitUtil.generateGitignoreFileIfNeeded(project, projectRoot) } override fun setUpModule() { @@ -32,32 +47,57 @@ class GitIgnoredFileTest : GitPlatformTest() { myModule.addContentRoot(moduleDir) val outDir = moduleDir.findOrCreateDir(OUT) val excludedDir = moduleDir.findOrCreateDir(EXCLUDED) - val excludedDirChild = excludedDir.findOrCreateDir(EXCLUDED_CHILD) + val excludedChildDir = excludedDir.findOrCreateDir(EXCLUDED_CHILD_DIR) myModule.addExclude(outDir) myModule.addExclude(excludedDir) - myModule.addExclude(excludedDirChild) + myModule.addExclude(excludedChildDir) } } - fun `test gitignore created`() { - assertTrue(File("$projectPath/$GITIGNORE").exists()) + fun `test gitignore content in config dir`() { + val gitIgnore = File("$projectPath/$DIRECTORY_STORE_FOLDER/$GITIGNORE") + if (gitIgnore.exists()) gitIgnore.delete() + + val shelf = File(ShelveChangesManager.getShelfPath(project)) + val shelfExist = if (shelf.exists()) true else shelf.mkdir() + if (!shelfExist) fail("Shelf doesn't exist and cannot be created") + + val workspaceFilePath = project.stateStore.workspaceFilePath + if (workspaceFilePath == null) fail("Cannot detect workspace file path") + val workspaceFile = File(workspaceFilePath!!) + val workspaceFileExist = FileUtil.createIfNotExists(workspaceFile) + if (!workspaceFileExist || VfsUtil.findFileByIoFile(workspaceFile, true) == null) fail("Workspace file doesn't exist and cannot be created") + + GitUtil.generateGitignoreFileIfNeeded(project, VfsUtil.findFile(Paths.get("$projectPath/$DIRECTORY_STORE_FOLDER"), true)!!) + + assertGitignoreValid(gitIgnore, + """ + # Default ignored files + /$SHELF/ + /${workspaceFile.name} + """) } - fun `test gitignore content`() { - val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset - val gitIgnoreExpectedContentList = """ - # Default ignored files - /.shelf/ - *.iws + fun `test gitignore content in project root`() { + GitUtil.generateGitignoreFileIfNeeded(project, projectRoot) + val gitIgnore = File("$projectPath/$GITIGNORE") + + assertGitignoreValid(gitIgnore, + """ # Project exclude paths /$EXCLUDED/ - /$EXCLUDED_CHILD_DIR/ + /$EXCLUDED_CHILD/ /$OUT/ - """.trimIndent().lines() - val gitIgnoreFile = File("$projectPath/$GITIGNORE") - assertTrue(gitIgnoreFile.exists()) - val generatedGitIgnoreContent = gitIgnoreFile.readText(projectCharset) + """) + } + + private fun assertGitignoreValid(ignoreFile: File, gitIgnoreExpectedContent: String) { + val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset + val gitIgnoreExpectedContentList = gitIgnoreExpectedContent.trimIndent().lines() + + assertTrue(ignoreFile.exists()) + val generatedGitIgnoreContent = ignoreFile.readText(projectCharset) assertFalse("Generated ignore file is empty", generatedGitIgnoreContent.isBlank()) assertFalse("Generated ignore file content should be system-independent", generatedGitIgnoreContent.contains('\\')) assertContainsOrdered(generatedGitIgnoreContent.lines(), gitIgnoreExpectedContentList) diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt index 411402c7d3d7..8727700427c1 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.kt @@ -236,7 +236,6 @@ class GithubShareAction : DumbAwareAction("Share Project on GitHub", "Easily sha return false } GitInit.refreshAndConfigureVcsMappings(project, root, root.path) - //even if gitignore file can be created after VCS configuration changed, we call this explicitly just to be sure that gitignore file will be added to initial commit GitUtil.generateGitignoreFileIfNeeded(project, root) return true }