vcs-ignore: change gitignore generation

* generate ignore file in .idea directory silently
* silently generate gitignore after GitHub share action but before initial commit take place
* propose generate ignore file after git init action from IDE
* new setting and notification for manage ignore files
* fix IDEA-201596
This commit is contained in:
Dmitry Zhuravlev
2018-12-07 19:37:12 +03:00
parent 513b8e0c18
commit ce700deaea
22 changed files with 369 additions and 143 deletions
@@ -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
@@ -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
@@ -22,6 +22,7 @@ public class VcsApplicationSettings implements PersistentStateComponent<VcsAppli
public boolean SHOW_LST_WORD_DIFFERENCES = true;
public boolean DETECT_PATCH_ON_THE_FLY = false;
public boolean ENABLE_PARTIAL_CHANGELISTS = true;
public boolean MANAGE_IGNORE_FILES = false;
public static VcsApplicationSettings getInstance() {
return ServiceManager.getService(VcsApplicationSettings.class);
@@ -17,6 +17,8 @@ package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -24,17 +26,19 @@ public class IgnoredBeanFactory {
private IgnoredBeanFactory() {
}
public static IgnoredFileBean ignoreUnderDirectory(final @NonNls String path, Project p) {
final String correctedPath = (path.endsWith("/") || path.endsWith(File.separator)) ? path : path + "/";
@NotNull
public static IgnoredFileBean ignoreUnderDirectory(@NotNull @NonNls String path, @Nullable Project p) {
String correctedPath = (path.endsWith("/") || path.endsWith(File.separator)) ? path : path + "/";
return new IgnoredFileBean(correctedPath, IgnoreSettingsType.UNDER_DIR, p);
}
public static IgnoredFileBean ignoreFile(final @NonNls String path, Project p) {
// todo check??
@NotNull
public static IgnoredFileBean ignoreFile(@NotNull @NonNls String path, @Nullable Project p) {
return new IgnoredFileBean(path, IgnoreSettingsType.FILE, p);
}
public static IgnoredFileBean withMask(final String mask) {
@NotNull
public static IgnoredFileBean withMask(@NotNull String mask) {
return new IgnoredFileBean(mask);
}
}
@@ -26,20 +26,22 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.NullVirtualFile;
import com.intellij.util.PathUtilRt;
import com.intellij.util.PatternUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.regex.Pattern;
public class IgnoredFileBean {
public class IgnoredFileBean implements IgnoredFileDescriptor {
private final String myPath;
private final String myFilenameIfFile;
private final String myMask;
@Nullable private final Pattern myPattern;
private final Pattern myPattern;
private final IgnoreSettingsType myType;
private final Project myProject;
private volatile VirtualFile myCachedResolved;
IgnoredFileBean(String path, IgnoreSettingsType type, Project project) {
IgnoredFileBean(@NotNull String path, @NotNull IgnoreSettingsType type, @Nullable Project project) {
myPath = path;
myType = type;
myFilenameIfFile = IgnoreSettingsType.FILE.equals(type) ? PathUtilRt.getFileName(path) : null;
@@ -48,42 +50,47 @@ public class IgnoredFileBean {
myPattern = null;
}
Project getProject() {
return myProject;
}
IgnoredFileBean(String mask) {
IgnoredFileBean(@NotNull String mask) {
myType = IgnoreSettingsType.MASK;
myMask = mask;
myPattern = mask != null ? PatternUtil.fromMask(mask) : null;
myPattern = PatternUtil.fromMask(mask);
myPath = null;
myFilenameIfFile = null;
myProject = null;
}
@Nullable
Project getProject() {
return myProject;
}
@Override
@Nullable
public String getPath() {
return myPath;
}
@Override
@Nullable
public String getMask() {
return myMask;
}
@Override
@NotNull
public IgnoreSettingsType getType() {
return myType;
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IgnoredFileBean that = (IgnoredFileBean)o;
if (myPath != null ? !myPath.equals(that.myPath) : that.myPath != null) return false;
if (myMask != null ? !myMask.equals(that.myMask) : that.myMask != null) return false;
if (!Objects.equals(myPath, that.myPath)) return false;
if (!Objects.equals(myMask, that.myMask)) return false;
if (myType != that.myType) return false;
return true;
@@ -97,9 +104,10 @@ public class IgnoredFileBean {
return result;
}
public boolean matchesFile(VirtualFile file) {
@Override
public boolean matchesFile(@NotNull VirtualFile file) {
if (myType == IgnoreSettingsType.MASK) {
return myPattern != null && myPattern.matcher(file.getName()).matches();
return myPattern.matcher(file.getName()).matches();
}
else {
// quick check for 'file' == exact match pattern
@@ -121,6 +129,7 @@ public class IgnoredFileBean {
}
}
@NotNull
private VirtualFile resolve() {
if (myCachedResolved == null) {
VirtualFile resolved = doResolve();
@@ -0,0 +1,30 @@
// 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.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface IgnoredFileDescriptor {
/**
* Path to file or directory in case if {@link IgnoreSettingsType#FILE} or {@link IgnoreSettingsType#UNDER_DIR}
* @return full path to file or directory. null in case if {@link IgnoreSettingsType} is {@link IgnoreSettingsType#MASK}
*/
@Nullable
String getPath();
/**
* Ignored mask represent ignore pattern in ignore files for different VCS (e.g. .gitignore, .hgignore, etc).
* The recommend way to support all VCS is to choose common pattern for mask.
* Note: At the moment this mask will be written to ignore file without any pre-processing (as it is)
*
* @return ignored mask. null in case if {@link IgnoreSettingsType} is not {@link IgnoreSettingsType#MASK}
*/
@Nullable
String getMask();
@NotNull
IgnoreSettingsType getType();
boolean matchesFile(@NotNull VirtualFile file);
}
@@ -15,4 +15,6 @@ import java.io.IOException;
public interface IgnoredFileGenerator {
boolean generateFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException;
boolean appendFile(@NotNull VirtualFile ignoreFileRoot, @NotNull AbstractVcs vcs) throws IOException;
}
@@ -18,7 +18,6 @@ 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;
@@ -33,8 +32,8 @@ public interface IgnoredFileProvider {
boolean isIgnoredFile(@NotNull Project project, @NotNull FilePath filePath);
@NotNull
Set<String> getIgnoredFilesMasks(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot);
Set<IgnoredFileDescriptor> getIgnoredFiles(@NotNull Project project);
@NotNull
String getMasksGroupDescription();
String getIgnoredGroupDescription();
}
@@ -1548,41 +1548,23 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Projec
@NotNull
@Override
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)) return masks;
public Set<IgnoredFileDescriptor> getIgnoredFiles(@NotNull Project project) {
Set<IgnoredFileBean> 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";
}
}
@@ -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) {
@@ -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<String> {
private fun getProjectExcludePathsRelativeTo(project: Project): Set<IgnoredFileDescriptor> {
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()
}
}
@@ -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)
}
}
@@ -148,7 +148,7 @@
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<grid id="aa6eb" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="aa6eb" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@@ -166,7 +166,7 @@
</component>
<grid id="6c335" layout-manager="FlowLayout" hgap="5" vgap="2" flow-align="0">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
@@ -198,6 +198,14 @@
<text value="Restore workspace on &amp;branch switching"/>
</properties>
</component>
<component id="2334f" class="javax.swing.JCheckBox" binding="myManageIgnoreFiles">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Manage &amp;ignore files"/>
</properties>
</component>
</children>
</grid>
</children>
@@ -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
@@ -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));
}
}
@@ -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;
/**
* <p>{@link VcsUtil} extension that needs access to the {@code intellij.platform.vcs.impl} module.</p>
@@ -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);
}
}
@@ -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 {
@@ -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);
}
}
@@ -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);
}
});
});
@@ -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<IgnoredFileProvider>): 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<IgnoredFileDescriptor>.ignoreBeansToRelativePaths(ignoreFileRoot: VirtualFile, untrackedFiles: Set<VirtualFile>): List<String> {
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<VirtualFile>,
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<VirtualFile>) =
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"
}
@@ -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)
@@ -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
}