/* * The MIT License (MIT) * * Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package mobi.hsz.idea.gitignore.util.exec; import com.intellij.dvcs.repo.Repository; import com.intellij.execution.process.BaseOSProcessHandler; import com.intellij.execution.process.ProcessHandler; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; import git4idea.config.GitVcsApplicationSettings; import mobi.hsz.idea.gitignore.lang.IgnoreLanguage; import mobi.hsz.idea.gitignore.lang.kind.GitLanguage; import mobi.hsz.idea.gitignore.util.Utils; import mobi.hsz.idea.gitignore.util.exec.parser.ExecutionOutputParser; import mobi.hsz.idea.gitignore.util.exec.parser.GitExcludesOutputParser; import mobi.hsz.idea.gitignore.util.exec.parser.GitUnignoredFilesOutputParser; import mobi.hsz.idea.gitignore.util.exec.parser.SimpleOutputParser; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.service.SharedThreadPool; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; /** * Class that holds util methods for calling external executables (i.e. git/hg) * * @author Jakub Chrzanowski <jakub@hsz.mobi> * @since 1.4 */ public class ExternalExec { /** Default external exec timeout. */ private static final int DEFAULT_TIMEOUT = 5000; /** Private constructor to prevent creating Icons instance. */ private ExternalExec() { } /** Checks if Git plugin is enabled. */ private static final boolean GIT_ENABLED = Utils.isGitPluginEnabled(); /** Git command to get user's excludesfile path. */ @NonNls private static final String GIT_CONFIG_EXCLUDES_FILE = "config --global core.excludesfile"; /** Git command to list unversioned files. */ @NonNls private static final String GIT_UNIGNORED_FILES = "clean -dn"; /** Git command to list ignored but tracked files. */ @NonNls private static final String GIT_TRACKED_IGNORED_FILES = "ls-files --ignored --exclude-standard"; /** Git command to remove file from tracking. */ @NonNls private static final String GIT_REMOVE_FILE_FROM_TRACKING = "rm --cached --force"; /** * Returns {@link VirtualFile} instance of the Git excludes file if available. * * @return Git excludes file */ @Nullable public static VirtualFile getGitExcludesFile() { return runForSingle(GitLanguage.INSTANCE, GIT_CONFIG_EXCLUDES_FILE, null, new GitExcludesOutputParser()); } /** * Returns list of unignored files for the given directory. * * @param language to check * @param project current project * @param file current file * @return unignored files list */ @NotNull public static List<String> getUnignoredFiles(@NotNull IgnoreLanguage language, @NotNull Project project, @NotNull VirtualFile file) { if (!Utils.isInProject(file, project)) { return ContainerUtil.newArrayList(); } ArrayList<String> result = run( language, GIT_UNIGNORED_FILES, file.getParent(), new GitUnignoredFilesOutputParser() ); return Utils.notNullize(result); } /** * Returns list of ignored and tracked files for the given directory. * * @param repository repository to check * @return unignored files list */ @NotNull public static List<String> getTrackedIgnoredFiles(@NotNull Repository repository) { ArrayList<String> result = run( GitLanguage.INSTANCE, GIT_TRACKED_IGNORED_FILES, repository.getRoot(), new SimpleOutputParser() ); return Utils.notNullize(result); } /** * Removes given files from the git tracking. * * @param file to untrack * @param repository file's repository */ public static void removeFileFromTracking(@NotNull VirtualFile file, @NotNull Repository repository) { final VirtualFile root = repository.getRoot(); final String command = GIT_REMOVE_FILE_FROM_TRACKING + " " + Utils.getRelativePath(root, file); run(GitLanguage.INSTANCE, command, root); } /** * Returns path to the {@link IgnoreLanguage} binary or null if not available. * Currently only {@link GitLanguage} is supported. * * @param language current language * @return path to binary */ @Nullable private static String bin(@NotNull IgnoreLanguage language) { if (GitLanguage.INSTANCE.equals(language) && GIT_ENABLED) { final String bin = GitVcsApplicationSettings.getInstance().getPathToGit(); return StringUtil.nullize(bin); } return null; } /** * Runs {@link IgnoreLanguage} executable with the given command and current working directory. * * @param language current language * @param command to call * @param directory current working directory * @param parser {@link ExecutionOutputParser} implementation * @param <T> return type * @return result of the call */ @Nullable private static <T> T runForSingle(@NotNull IgnoreLanguage language, @NotNull String command, @Nullable VirtualFile directory, @NotNull final ExecutionOutputParser<T> parser) { return Utils.getFirstItem(run(language, command, directory, parser)); } /** * Runs {@link IgnoreLanguage} executable with the given command and current working directory. * * @param language current language * @param command to call * @param directory current working directory */ private static void run(@NotNull IgnoreLanguage language, @NotNull String command, @Nullable VirtualFile directory) { run(language, command, directory, null); } /** * Runs {@link IgnoreLanguage} executable with the given command and current working directory. * * @param language current language * @param command to call * @param directory current working directory * @param parser {@link ExecutionOutputParser} implementation * @param <T> return type * @return result of the call */ @Nullable private static <T> ArrayList<T> run(@NotNull IgnoreLanguage language, @NotNull String command, @Nullable VirtualFile directory, @Nullable final ExecutionOutputParser<T> parser) { final String bin = bin(language); if (bin == null) { return null; } try { final String cmd = bin + " " + command; final File workingDirectory = directory != null ? new File(directory.getPath()) : null; final Process process = Runtime.getRuntime().exec(cmd, null, workingDirectory); ProcessHandler handler = new BaseOSProcessHandler(process, StringUtil.join(cmd, " "), null) { @NotNull @Override protected Future<?> executeOnPooledThread(@NotNull Runnable task) { return SharedThreadPool.getInstance().executeOnPooledThread(task); } @Override public void notifyTextAvailable(String text, Key outputType) { if (parser != null) { parser.onTextAvailable(text, outputType); } } }; handler.startNotify(); if (!handler.waitFor(DEFAULT_TIMEOUT)) { return null; } if (parser != null) { parser.notifyFinished(process.exitValue()); if (parser.isErrorsReported()) { return null; } return parser.getOutput(); } } catch (IOException ignored) { } return null; } }