vcs-ignore: implement ability to update ignore files
authorDmitry Zhuravlev <dmitry.zhuravlev@jetbrains.com>
Fri, 2 Nov 2018 13:33:54 +0000 (16:33 +0300)
committerDmitry Zhuravlev <dmitry.zhuravlev@jetbrains.com>
Thu, 18 Apr 2019 12:12:11 +0000 (15:12 +0300)
platform/platform-resources-en/src/messages/VcsBundle.properties
platform/vcs-api/src/com/intellij/openapi/vcs/changes/IgnoredFileContentProvider.java
platform/vcs-impl/gen/com/intellij/openapi/vcs/changes/ignore/parser/IgnoreParser.java
platform/vcs-impl/src/com/intellij/openapi/vcs/FilesProcessorWithNotificationImpl.kt
platform/vcs-impl/src/com/intellij/openapi/vcs/VcsVFSListener.java
platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/IgnoreFilesProcessorImpl.kt [new file with mode: 0644]
platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/psi/util/IgnoreFilePsiUtil.kt [new file with mode: 0644]
platform/vcs-impl/src/com/intellij/vcsUtil/VcsImplUtil.java
plugins/git4idea/src/git4idea/ignore/GitIgnoredFileContentProvider.kt
plugins/git4idea/tests/git4idea/ignore/GitIgnoredFileTest.kt

index ce40370102a209cbfee06f176055a6f1b9940a09..a75e0eb7bf801939e805fda603bad3b113b97646 100644 (file)
@@ -379,10 +379,12 @@ 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.manage.view=View
+ignored.file.manage.with.files.message=Ignored files detected. Manage VCS ignore files automatically.
 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
+ignored.file.manage.this.project=This project
+ignored.file.manage.all.project=All projects
+ignored.file.manage.notmanage=Not manage
 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
index 3c958fafae613d026a7b1e6d0bd52eb98b809381..848969f51404197a19b61de8597ea00f78671327 100644 (file)
@@ -23,4 +23,7 @@ public interface IgnoredFileContentProvider {
 
   @NotNull
   String buildUnignoreContent(@NotNull String ignorePattern);
+
+  @NotNull
+  String buildIgnoreGroupDescription(@NotNull IgnoredFileProvider ignoredFileProvider);
 }
index 03f7486cb79aa138c5aad8b0a857c94dd82dcbda..f145442dd6df127335fb9c7c16bcc11eec782482 100644 (file)
@@ -48,7 +48,7 @@ public class IgnoreParser implements PsiParser {
   }
 
   protected boolean parse_root_(IElementType root_, PsiBuilder builder_, int level_) {
-    return gitignoreFile(builder_, level_ + 1);
+    return ignoreFile(builder_, level_ + 1);
   }
 
   public static final TokenSet[] EXTENDS_SETS_ = new TokenSet[] {
@@ -226,12 +226,12 @@ public class IgnoreParser implements PsiParser {
 
   /* ********************************************************** */
   // item_ *
-  static boolean gitignoreFile(PsiBuilder builder_, int level_) {
-    if (!recursion_guard_(builder_, level_, "gitignoreFile")) return false;
+  static boolean ignoreFile(PsiBuilder builder_, int level_) {
+    if (!recursion_guard_(builder_, level_, "ignoreFile")) return false;
     int pos_ = current_position_(builder_);
     while (true) {
       if (!item_(builder_, level_ + 1)) break;
-      if (!empty_element_parsed_guard_(builder_, "gitignoreFile", pos_)) break;
+      if (!empty_element_parsed_guard_(builder_, "ignoreFile", pos_)) break;
       pos_ = current_position_(builder_);
     }
     return true;
index a66c59be7d0cf53bbc5bf9361cba6584c76d5c35..ab5f566bdac8642c7b8d9e4ea95f7ae841740166 100644 (file)
@@ -159,5 +159,5 @@ abstract class FilesProcessorWithNotificationImpl(protected val project: Project
 
   private fun notAskedBefore() = !projectProperties.getBoolean(askedBeforeProperty, false)
 
-  private fun needDoForCurrentProject() = projectProperties.getBoolean(doForCurrentProjectProperty, false)
+  protected open fun needDoForCurrentProject() = projectProperties.getBoolean(doForCurrentProjectProperty, false)
 }
\ No newline at end of file
index 61c639249a024afd547592832c93738cee555457..0fd1bd51e82d54f791258e62d19ac89b77be9443 100644 (file)
@@ -13,6 +13,7 @@ import com.intellij.openapi.util.Comparing;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vcs.actions.VcsContextFactory;
 import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.openapi.vcs.changes.ignore.IgnoreFilesProcessorImpl;
 import com.intellij.openapi.vfs.*;
 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
 import com.intellij.util.SmartList;
@@ -84,6 +85,7 @@ public abstract class VcsVFSListener implements Disposable {
 
     myProjectConfigurationFilesProcessor = createProjectConfigurationFilesProcessor();
     myExternalFilesProcessor = createExternalFilesProcessor();
+    createIgnoredFilesProcessor();
   }
 
   @Override
@@ -331,6 +333,10 @@ public abstract class VcsVFSListener implements Disposable {
 
   protected abstract boolean isDirectoryVersioningSupported();
 
+  private void createIgnoredFilesProcessor() {
+    new IgnoreFilesProcessorImpl(myProject, this);
+  }
+
   @SuppressWarnings("unchecked")
   private FilesProcessor createExternalFilesProcessor() {
     return new ExternallyAddedFilesProcessorImpl(myProject,
diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/IgnoreFilesProcessorImpl.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/IgnoreFilesProcessorImpl.kt
new file mode 100644 (file)
index 0000000..025f433
--- /dev/null
@@ -0,0 +1,169 @@
+// 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.ignore
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.application.runWriteAction
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.vcs.FilesProcessorWithNotificationImpl
+import com.intellij.openapi.vcs.VcsApplicationSettings
+import com.intellij.openapi.vcs.VcsBundle
+import com.intellij.openapi.vcs.changes.ChangeListManagerImpl
+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.vcs.changes.ignore.psi.util.addNewElementsToIgnoreBlock
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VfsUtilCore
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.newvfs.events.*
+import com.intellij.project.getProjectStoreDirectory
+import com.intellij.vcsUtil.VcsImplUtil
+import com.intellij.vcsUtil.VcsImplUtil.MANAGE_IGNORE_FILES_PROPERTY
+import com.intellij.vcsUtil.VcsUtil
+import com.intellij.vfs.AsyncVfsEventsListener
+import com.intellij.vfs.AsyncVfsEventsPostProcessor
+
+const val ASKED_MANAGE_IGNORE_FILES_PROPERTY = "ASKED_MANAGE_IGNORE_FILES"
+
+private val LOG = logger<IgnoreFilesProcessorImpl>()
+
+class IgnoreFilesProcessorImpl(project: Project, parentDisposable: Disposable)
+  : FilesProcessorWithNotificationImpl(project, parentDisposable), AsyncVfsEventsListener, Disposable {
+
+  private val changeListManager = ChangeListManagerImpl.getInstanceImpl(project)
+
+  init {
+    runReadAction {
+      if (!project.isDisposed) {
+        AsyncVfsEventsPostProcessor.getInstance().addListener(this, parentDisposable)
+      }
+    }
+  }
+
+  override fun filesChanged(events: List<VFileEvent>) {
+    if (!needProcessIgnoredFiles() || ApplicationManager.getApplication().isUnitTestMode) return
+
+    val potentiallyIgnoredFiles =
+      events.asSequence()
+        .mapNotNull(::getAffectedFile)
+        .filter { changeListManager.isPotentiallyIgnoredFile(it) }
+        .toList()
+
+    if (potentiallyIgnoredFiles.isEmpty()) return
+    LOG.debug("Got potentially ignored files from VFS events", potentiallyIgnoredFiles)
+
+    processFiles(potentiallyIgnoredFiles)
+  }
+
+  override fun doActionOnChosenFiles(files: Collection<VirtualFile>) {
+    runInEdt {
+      writeIgnores(project, files)
+    }
+  }
+
+  override fun dispose() {}
+
+  private fun writeIgnores(project: Project, potentiallyIgnoredFiles: Collection<VirtualFile>) {
+    if (potentiallyIgnoredFiles.isEmpty()) return
+
+    LOG.debug("Try to write potential ignored files", potentiallyIgnoredFiles)
+    val ignoreFileToContent = hashMapOf<VirtualFile, MutableList<IgnoreGroupContent>>()
+    val providerToDescriptorMap = IgnoredFileProvider.IGNORE_FILE.extensions.associate { it to it.getIgnoredFiles(project) }
+
+    for (potentiallyIgnoredFile in potentiallyIgnoredFiles) {
+      VcsUtil.getVcsFor(project, potentiallyIgnoredFile)?.let { vcs ->
+        VcsImplUtil.getIgnoredFileContentProvider(project, vcs)?.let { ignoredContentProvider ->
+          findOrCreateIgnoreFileByFile(project, ignoredContentProvider, potentiallyIgnoredFile)?.let { ignoreFile ->
+            for ((ignoredFileProvider, descriptors) in providerToDescriptorMap) {
+              for (ignoredFileDescriptor in descriptors.filter { it.matchesFile(potentiallyIgnoredFile) }) {
+                val ignoreFileContent = ignoreFileToContent.computeIfAbsent(ignoreFile) { mutableListOf() }
+                val groupDescription = " ${ignoredFileProvider.ignoredGroupDescription}"
+                val ignoreFileGroupContent = ignoreFileContent.getOrInitialize(groupDescription)
+                ignoreFileGroupContent.ignoredDescriptors.add(ignoredFileDescriptor)
+              }
+            }
+          }
+        }
+      }
+    }
+
+    for ((ignoreFile, newContent) in ignoreFileToContent) {
+      for (groupContent in newContent) {
+        val ignoredDescriptors = groupContent.ignoredDescriptors
+        LOG.debug("Write to ignore file ${ignoreFile} ignores: $ignoredDescriptors")
+        addNewElementsToIgnoreBlock(project, ignoreFile, groupContent.group, *ignoredDescriptors.toTypedArray())
+      }
+    }
+  }
+
+  private fun MutableList<IgnoreGroupContent>.getOrInitialize(group: String): IgnoreGroupContent =
+    find { it.group == group } ?: IgnoreGroupContent(group).apply { this@getOrInitialize.add(this) }
+
+  private data class IgnoreGroupContent(val group: String, val ignoredDescriptors: MutableSet<IgnoredFileDescriptor> = mutableSetOf())
+
+  private fun findOrCreateIgnoreFileByFile(project: Project,
+                                           ignoredContentProvider: IgnoredFileContentProvider,
+                                           file: VirtualFile): VirtualFile? {
+    val storeDir = findStoreDir(project)
+
+    val ignoreFileRoot =
+      if (storeDir != null && file.underProjectStoreDir(storeDir)) storeDir else VcsUtil.getVcsRootFor(project, file) ?: return null
+
+    return ignoreFileRoot.findChild(ignoredContentProvider.fileName) ?: runWriteAction {
+      ignoreFileRoot.createChildData(this, ignoredContentProvider.fileName)
+    }
+  }
+
+  private fun findStoreDir(project: Project): VirtualFile? {
+    val projectBasePath = project.basePath ?: return null
+    val projectBaseDir = LocalFileSystem.getInstance().findFileByPath(projectBasePath) ?: return null
+
+    return getProjectStoreDirectory(projectBaseDir) ?: return null
+  }
+
+  private fun VirtualFile.underProjectStoreDir(storeDir: VirtualFile): Boolean {
+    return VfsUtilCore.isAncestor(storeDir, this, true)
+  }
+
+  override fun doFilterFiles(files: Collection<VirtualFile>) = files.filter { shouldIgnore(it) }
+
+  override fun rememberForAllProjects() {
+    val applicationSettings = VcsApplicationSettings.getInstance()
+    applicationSettings.MANAGE_IGNORE_FILES = true
+  }
+
+  override val askedBeforeProperty = ASKED_MANAGE_IGNORE_FILES_PROPERTY
+
+  override val doForCurrentProjectProperty = MANAGE_IGNORE_FILES_PROPERTY
+  override val showActionText: String = VcsBundle.getString("ignored.file.manage.view")
+
+  override val forCurrentProjectActionText: String = VcsBundle.getString("ignored.file.manage.this.project")
+  override val forAllProjectsActionText: String? = VcsBundle.getString("ignored.file.manage.all.project")
+  override val muteActionText: String = VcsBundle.getString("ignored.file.manage.notmanage")
+
+  override fun notificationTitle() = ""
+  override fun notificationMessage(): String = VcsBundle.message("ignored.file.manage.with.files.message")
+
+  private fun shouldIgnore(file: VirtualFile) = changeListManager.isPotentiallyIgnoredFile(file)
+
+  override fun needDoForCurrentProject() = VcsApplicationSettings.getInstance().MANAGE_IGNORE_FILES || super.needDoForCurrentProject()
+
+  private fun getAffectedFile(event: VFileEvent): VirtualFile? =
+    runReadAction {
+      when {
+        event is VFileCreateEvent && event.parent.isValid -> event.file
+        event is VFileMoveEvent || event.isRename() -> event.file
+        event is VFileCopyEvent && event.newParent.isValid -> event.newParent.findChild(event.newChildName)
+        else -> null
+      }
+    }
+
+  private fun VFileEvent.isRename() = this is VFilePropertyChangeEvent && isRename
+
+  private fun needProcessIgnoredFiles() = Registry.`is`("vcs.ignorefile.generation", true)
+}
\ No newline at end of file
diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/psi/util/IgnoreFilePsiUtil.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ignore/psi/util/IgnoreFilePsiUtil.kt
new file mode 100644 (file)
index 0000000..39cbe25
--- /dev/null
@@ -0,0 +1,167 @@
+// 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.ignore.psi.util
+
+import com.intellij.lang.ASTFactory
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.application.runUndoTransparentWriteAction
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.vcs.changes.IgnoreSettingsType
+import com.intellij.openapi.vcs.changes.IgnoredFileDescriptor
+import com.intellij.openapi.vcs.changes.ignore.lang.IgnoreFileConstants.NEWLINE
+import com.intellij.openapi.vcs.changes.ignore.lang.IgnoreLanguage
+import com.intellij.openapi.vcs.changes.ignore.psi.IgnoreTypes
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.*
+import com.intellij.psi.impl.GeneratedMarkerVisitor
+import com.intellij.psi.impl.PsiFileFactoryImpl
+import com.intellij.psi.impl.source.DummyHolderFactory
+import com.intellij.psi.tree.IElementType
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.annotations.TestOnly
+
+@TestOnly
+fun updateIgnoreBlock(project: Project,
+                      ignoreFile: VirtualFile,
+                      ignoredGroupDescription: String,
+                      vararg newEntries: IgnoredFileDescriptor): PsiFile? {
+  val ignoreFilePsi = ignoreFile.findIgnorePsi(project) ?: return null
+  val psiFactory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl
+  val psiParserFacade = PsiParserFacade.SERVICE.getInstance(project)
+  invokeAndWaitIfNeeded {
+    runUndoTransparentWriteAction {
+      updateIgnoreBlock(psiParserFacade, ignoreFilePsi, ignoredGroupDescription,
+                        newEntries.map { it.toPsiElement(psiFactory, ignoreFilePsi) })
+    }
+  }
+  return ignoreFilePsi
+}
+
+fun addNewElementsToIgnoreBlock(project: Project,
+                                ignoreFile: VirtualFile,
+                                ignoredGroupDescription: String,
+                                vararg newEntries: IgnoredFileDescriptor): PsiFile? {
+  val ignoreFilePsi = ignoreFile.findIgnorePsi(project) ?: return null
+  val psiFactory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl
+  val psiParserFacade = PsiParserFacade.SERVICE.getInstance(project)
+  invokeAndWaitIfNeeded {
+    runUndoTransparentWriteAction {
+      addNewElementsToIgnoreBlock(ignoredGroupDescription, psiParserFacade, ignoreFilePsi,
+                                  newEntries.map {
+                                    it.toPsiElement(psiFactory, ignoreFilePsi)
+                                  })
+    }
+  }
+  return ignoreFilePsi
+}
+
+private fun updateIgnoreBlock(psiParserFacade: PsiParserFacade,
+                              ignoreFilePsi: PsiFile,
+                              ignoredGroupDescription: String,
+                              newEntries: List<PsiElement>) {
+  val ignoredGroupPsiElement = ignoreFilePsi.findOrCreateIgnoreBlockDescriptionPsi(ignoredGroupDescription, psiParserFacade)
+
+  var replacementCandidate = ignoredGroupPsiElement.nextIgnoreGroupElement()
+  var lastElementInBlock = if (ignoredGroupPsiElement.nextSibling.isNewLine()) ignoredGroupPsiElement.nextSibling else ignoredGroupPsiElement
+
+  for (newEntry in newEntries) {
+    if (replacementCandidate != null) {
+      lastElementInBlock = replacementCandidate.replace(newEntry)
+    }
+    else {
+      val newLinePsi = lastElementInBlock.createNewline()
+      lastElementInBlock = ignoreFilePsi.addAfter(newLinePsi, lastElementInBlock)
+      lastElementInBlock = ignoreFilePsi.addAfter(newEntry, lastElementInBlock)
+    }
+    replacementCandidate = replacementCandidate.nextIgnoreGroupElement()
+  }
+}
+
+private fun addNewElementsToIgnoreBlock(ignoredGroupDescription: String,
+                                        psiParserFacade: PsiParserFacade,
+                                        ignoreFilePsi: PsiFile,
+                                        newEntries: List<PsiElement>) {
+  val ignoredGroupPsiElement = ignoreFilePsi.findOrCreateIgnoreBlockDescriptionPsi(ignoredGroupDescription, psiParserFacade)
+
+  var nextIgnoreGroupElement = ignoredGroupPsiElement.nextIgnoreGroupElement()
+  var lastElementInBlock = if (ignoredGroupPsiElement.nextSibling.isNewLine()) ignoredGroupPsiElement.nextSibling else ignoredGroupPsiElement
+  val existingElements = mutableListOf<PsiElement>()
+
+  while (nextIgnoreGroupElement != null) {
+    val existingElement = newEntries.find { nextIgnoreGroupElement?.textMatches(it) == true }
+    if (existingElement != null) {
+      existingElements.add(existingElement)
+    }
+    lastElementInBlock = nextIgnoreGroupElement
+    nextIgnoreGroupElement = nextIgnoreGroupElement.nextIgnoreGroupElement()
+  }
+
+  for (elementToAdd in newEntries - existingElements) {
+    val newLinePsi = lastElementInBlock.createNewline()
+    lastElementInBlock = ignoreFilePsi.addAfter(newLinePsi, lastElementInBlock)
+    lastElementInBlock = ignoreFilePsi.addAfter(elementToAdd, lastElementInBlock)
+  }
+}
+
+private fun IgnoredFileDescriptor.toPsiElement(psiFactory: PsiFileFactoryImpl, ignorePsi: PsiFile): PsiElement {
+  val ignorePath = path
+  val ignoreMask = mask
+
+  val text =
+    if (ignorePath != null) {
+      val ignoreFileContainingDirPath = ignorePsi.virtualFile?.parent?.path ?: throw IllegalStateException(
+        "Cannot determine ignore file path for $ignorePsi")
+      "/${FileUtil.getRelativePath(ignoreFileContainingDirPath, ignorePath, '/')}"
+    }
+    else ignoreMask ?: throw IllegalStateException("IgnoredFileBean: path and mask cannot be null at the same time")
+
+  return ignorePsi.createElementFromText(text,
+                                         when (type) {
+                                           IgnoreSettingsType.UNDER_DIR -> IgnoreTypes.ENTRY_DIRECTORY
+                                           IgnoreSettingsType.FILE -> IgnoreTypes.ENTRY_FILE
+                                           IgnoreSettingsType.MASK -> IgnoreTypes.ENTRY
+                                         }, psiFactory)
+}
+
+private fun VirtualFile.findIgnorePsi(project: Project): PsiFile? =
+  runReadAction { PsiManager.getInstance(project).findFile(this).takeIf { it?.language is IgnoreLanguage } }
+
+private fun PsiFile.findOrCreateIgnoreBlockDescriptionPsi(ignoredGroupDescription: String, psiParserFacade: PsiParserFacade): PsiElement {
+  return PsiTreeUtil.findChildrenOfType(this, PsiComment::class.java)
+           .firstOrNull { it.text.contains(ignoredGroupDescription) }
+         ?: createIgnoreBlock(ignoredGroupDescription, psiParserFacade)
+}
+
+private fun PsiFile.createIgnoreBlock(ignoredGroupDescription: String, psiParserFacade: PsiParserFacade): PsiElement {
+  if (!prevSibling?.text.isNullOrBlank() && !prevSibling.isNewLine()) {
+    add(createNewline())
+  }
+  return add(psiParserFacade.createLineOrBlockCommentFromText(language, ignoredGroupDescription))
+}
+
+private fun PsiElement.createNewline(): PsiElement {
+  val holderElement = DummyHolderFactory.createHolder(PsiManager.getInstance(project), this).treeElement
+  val newElement = ASTFactory.leaf(IgnoreTypes.CRLF, holderElement.charTable.intern(NEWLINE))
+  holderElement.rawAddChildren(newElement)
+  GeneratedMarkerVisitor.markGenerated(newElement.psi)
+  return newElement.psi
+}
+
+private fun PsiElement.createElementFromText(text: String, type: IElementType, psiFactory: PsiFileFactoryImpl) =
+  psiFactory.createElementFromText(text, language, type, this)
+  ?: throw IllegalStateException("Cannot create PSI element for $text, $type, $this")
+
+private fun PsiElement?.nextIgnoreGroupElement(): PsiElement? {
+  var next = this?.nextSibling
+
+  while (next != null && next.isNewLine()) {
+    if (next is PsiComment) return null
+    if (next.nextSibling is PsiComment) return null
+
+    next = next.nextSibling
+  }
+  return next
+}
+
+private fun PsiElement?.isNewLine() = this?.text?.contains(NEWLINE) ?: false
index 5848cd652f22e9d44dacb10bdea7239c787e10cb..dfd0d8fb9817f7246f16de118ed2a03c43422b85 100644 (file)
@@ -30,6 +30,7 @@ import java.nio.file.Paths;
 
 import static com.intellij.openapi.vcs.FileStatus.IGNORED;
 import static com.intellij.openapi.vcs.FileStatus.UNKNOWN;
+import static com.intellij.openapi.vcs.changes.ignore.IgnoreFilesProcessorImplKt.ASKED_MANAGE_IGNORE_FILES_PROPERTY;
 import static com.intellij.vcsUtil.VcsUtil.isFileUnderVcs;
 
 /**
@@ -106,7 +107,7 @@ public class VcsImplUtil {
     if (canManageIgnoreFiles(project)) {
       updateIgnoreFileIfNeeded(project, vcs, ignoreFileRoot, ignoreFile.exists());
     }
-    else {
+    else if (notAskedToManageIgnoreFiles(project)) {
       notifyVcsIgnoreFileManage(project, () -> updateIgnoreFileAndOpen(project, vcs, ignoreFileRoot, ignoreFile));
     }
   }
@@ -117,19 +118,25 @@ public class VcsImplUtil {
     VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance();
 
     VcsNotifier.getInstance(project).notifyMinorInfo(
+      true,
       "",
       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);
+        propertiesComponent.setValue(ASKED_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;
+        propertiesComponent.setValue(ASKED_MANAGE_IGNORE_FILES_PROPERTY, true);
         notification.expire();
       }),
-      NotificationAction.create(VcsBundle.message("ignored.file.manage.notnow"), (event, notification) -> notification.expire()));
+      NotificationAction.create(VcsBundle.message("ignored.file.manage.notmanage"), (event, notification) -> {
+        propertiesComponent.setValue(ASKED_MANAGE_IGNORE_FILES_PROPERTY, true);
+        notification.expire();
+      }));
   }
 
   public static boolean generateIgnoreFileIfNeeded(@NotNull Project project,
@@ -167,7 +174,7 @@ public class VcsImplUtil {
     }
   }
 
-  private static IgnoredFileContentProvider getIgnoredFileContentProvider(@NotNull Project project,
+  public static IgnoredFileContentProvider getIgnoredFileContentProvider(@NotNull Project project,
                                                                           @NotNull AbstractVcs vcs) {
     return IgnoredFileContentProvider.IGNORE_FILE_CONTENT_PROVIDER.extensions(project)
       .filter((provider) -> provider.getSupportedVcs().equals(vcs.getKeyInstanceMethod()))
@@ -179,7 +186,14 @@ public class VcsImplUtil {
     PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project);
     VcsApplicationSettings applicationSettings = VcsApplicationSettings.getInstance();
 
-    return applicationSettings.MANAGE_IGNORE_FILES || propertiesComponent.getBoolean(MANAGE_IGNORE_FILES_PROPERTY, false);
+    return applicationSettings.MANAGE_IGNORE_FILES || (propertiesComponent.getBoolean(MANAGE_IGNORE_FILES_PROPERTY, false)
+                                                       && Registry.is("vcs.ignorefile.generation"));
+  }
+
+  private static boolean notAskedToManageIgnoreFiles(@NotNull Project project) {
+    PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project);
+
+    return !propertiesComponent.getBoolean(ASKED_MANAGE_IGNORE_FILES_PROPERTY, false);
   }
 
   private static boolean isFileSharedInVcs(@NotNull Project project, @NotNull ChangeListManagerEx changeListManager, @NotNull String filePath) {
index d9ee3a8149d9d4f346e215237eb86aa69ec1ac3e..bae48adcd0b97ad1e293a5e01901cf906590bf8b 100644 (file)
@@ -53,7 +53,7 @@ open class GitIgnoredFileContentProvider(private val project: Project) : Ignored
 
       val description = provider.ignoredGroupDescription
       if (description.isNotBlank()) {
-        content.append(prependCommentHashCharacterIfNeeded(description))
+        content.append(buildIgnoreGroupDescription(provider))
         content.append(lineSeparator)
       }
       content.append(ignoredFiles.joinToString(lineSeparator))
@@ -127,6 +127,9 @@ open class GitIgnoredFileContentProvider(private val project: Project) : Ignored
     append("!$ignorePattern")
   }.toString()
 
+  override fun buildIgnoreGroupDescription(ignoredFileProvider: IgnoredFileProvider) =
+    prependCommentHashCharacterIfNeeded(ignoredFileProvider.ignoredGroupDescription)
+
   private fun prependCommentHashCharacterIfNeeded(description: String): String =
     if (description.startsWith("#")) description else "# $description"
 }
index 1ca9524735c9c480239313715eb561674c3f3387..c38e2452d63d0537f84f64c7599ad383d5177368 100644 (file)
@@ -1,11 +1,17 @@
 // 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 git4idea.ignore
 
+import com.intellij.configurationStore.saveComponentManager
 import com.intellij.openapi.application.WriteAction
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.application.runWriteAction
 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.VcsConfiguration
+import com.intellij.openapi.vcs.changes.IgnoredBeanFactory
+import com.intellij.openapi.vcs.changes.ignore.psi.util.addNewElementsToIgnoreBlock
+import com.intellij.openapi.vcs.changes.ignore.psi.util.updateIgnoreBlock
 import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager
 import com.intellij.openapi.vfs.LocalFileSystem
 import com.intellij.openapi.vfs.VfsUtil
@@ -14,10 +20,8 @@ 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 git4idea.test.GitSingleRepoTest
 import java.io.File
-import java.nio.file.Path
 import java.nio.file.Paths
 
 const val OUT = "out"
@@ -26,24 +30,22 @@ const val EXCLUDED_CHILD_DIR = "child"
 const val EXCLUDED_CHILD = "$EXCLUDED/$EXCLUDED_CHILD_DIR"
 const val SHELF = "shelf"
 
-class GitIgnoredFileTest : GitPlatformTest() {
+class GitIgnoredFileTest : GitSingleRepoTest() {
 
-  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 getProjectDirOrFile() = getProjectDirOrFile(true)
 
   override fun setUp() {
     super.setUp()
     Registry.get("vcs.ignorefile.generation").setValue(true, testRootDisposable)
-    createRepository(project, projectPath)
+  }
+
+  override fun setUpProject() {
+    super.setUpProject()
+    invokeAndWaitIfNeeded { saveComponentManager(project) } //will create .idea directory
   }
 
   override fun setUpModule() {
-    WriteAction.runAndWait<RuntimeException> {
+    runWriteAction {
       myModule = createMainModule()
       val moduleDir = myModule.moduleFile!!.parent
       myModule.addContentRoot(moduleDir)
@@ -68,16 +70,17 @@ class GitIgnoredFileTest : GitPlatformTest() {
     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")
+    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}
-    """)
+         # Default ignored files
+         /$SHELF/
+         /${workspaceFile.name}
+     """)
   }
 
   fun `test gitignore content in project root`() {
@@ -94,6 +97,354 @@ class GitIgnoredFileTest : GitPlatformTest() {
     """)
   }
 
+  fun `test update first ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newFirstBlock = """
+      # first block
+      /test/
+      /file.txt
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# first block"
+    val psiIgnore = updateIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                      IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                      IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $newFirstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """)
+  }
+
+  fun `test update middle ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newMiddleBlock = """
+      # middle block
+      /test/
+      /file.txt
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# middle block"
+    val psiIgnore = updateIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                      IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                      IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $firstBlock
+
+    $newMiddleBlock
+
+    $lastBlock
+    """)
+  }
+
+  fun `test update last ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newLastBlock = """
+      # last block
+      /test/
+      /file.txt
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# last block"
+    val psiIgnore = updateIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                      IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                      IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $firstBlock
+
+    $middleBlock
+
+    $newLastBlock
+    """)
+  }
+
+  fun `test add elements to first ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newFirstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+      /test/
+      /file.txt
+      /file2.txt
+      /file3.txt
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# first block"
+    val psiIgnore = addNewElementsToIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file2.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file3.txt", project),
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/$EXCLUDED_CHILD/", project),
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/$EXCLUDED", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $newFirstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """)
+  }
+
+  fun `test add elements to middle ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newMiddleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+      /test/
+      /file.txt
+      /file2.txt
+      /file3.txt
+      /$EXCLUDED_CHILD/
+      /$EXCLUDED/
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# middle block"
+    val psiIgnore = addNewElementsToIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file2.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file3.txt", project),
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/$EXCLUDED_CHILD", project),
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/$EXCLUDED", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $firstBlock
+
+    $newMiddleBlock
+
+    $lastBlock
+    """)
+  }
+
+  fun `test add elements to last ignore block`() {
+    val projectCharset = EncodingProjectManager.getInstance(project).defaultCharset
+    val firstBlock = """
+      # first block
+      /$EXCLUDED/
+      /$EXCLUDED_CHILD/
+      /$OUT/
+    """
+    val middleBlock = """
+      # middle block
+      /middleBlockFolder/
+      /generatedMiddle/
+      /folder/*.txt
+      *.xml
+    """
+    val lastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+    """
+    val newLastBlock = """
+      # last block
+      /testInBlock2/
+      /generated/
+      *.txt
+      /test/
+      /file.txt
+      /file2.txt
+      /file3.txt
+      /file4.txt
+    """
+    val gitIgnore = File("$projectPath/$GITIGNORE")
+    gitIgnore.writeText(
+      """
+    $firstBlock
+
+    $middleBlock
+
+    $lastBlock
+    """.trimIndent(), projectCharset
+    )
+
+    val ignoreVF = getVirtualFile(gitIgnore) ?: return
+    val ignoreGroup = "# last block"
+    val psiIgnore = addNewElementsToIgnoreBlock(project, ignoreVF, ignoreGroup,
+                                                IgnoredBeanFactory.ignoreUnderDirectory("$projectPath/test", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file2.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file3.txt", project),
+                                                IgnoredBeanFactory.ignoreFile("$projectPath/file4.txt", project))
+    gitIgnore.writeText(psiIgnore?.text ?: "", projectCharset)
+
+    assertGitignoreValid(gitIgnore, """
+    $firstBlock
+
+    $middleBlock
+
+    $newLastBlock
+    """)
+  }
+
   fun `test do not add already ignored directories to gitignore`() {
     val shelfDir = WriteAction.computeAndWait<VirtualFile, RuntimeException> {
       val moduleDir = myModule.moduleFile!!.parent