/* * Copyright 2000-2011 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.vcsUtil; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.ThrowableNotNullFunction; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; import com.intellij.openapi.vcs.checkin.CheckinEnvironment; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ThrowableConsumer; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * @author Kirill Likhodedov */ public class VcsFileUtil { /** * If multiple paths are specified on the command line, this limit is used to split paths into chunks. * The limit is less than OS limit to leave space to quoting, spaces, charset conversion, and commands arguments. */ public static final int FILE_PATH_LIMIT = 7600; /** * Execute function for each chunk of arguments. Check for being cancelled in process. * * @param arguments the arguments to chunk * @param processor function to execute on each chunk * @param <T> type of result value * @return list of result values * @throws VcsException */ @NotNull public static <T> List<T> foreachChunk(@NotNull List<String> arguments, @NotNull ThrowableNotNullFunction<List<String>, List<? extends T>, VcsException> processor) throws VcsException { return foreachChunk(arguments, 1, processor); } /** * Execute function for each chunk of arguments and collect the result. Check for being cancelled in process. * * @param arguments the arguments to chunk * @param groupSize size of argument groups that should be put in the same chunk (like a name and a value) * @param processor function to execute on each chunk * @param <T> type of result value * @return list of result values * @throws VcsException */ @NotNull public static <T> List<T> foreachChunk(@NotNull List<String> arguments, int groupSize, @NotNull ThrowableNotNullFunction<List<String>, List<? extends T>, VcsException> processor) throws VcsException { List<T> result = ContainerUtil.newArrayList(); foreachChunk(arguments, groupSize, chunk -> { result.addAll(processor.fun(chunk)); }); return result; } /** * Execute function for each chunk of arguments. Check for being cancelled in process. * * @param arguments the arguments to chunk * @param groupSize size of argument groups that should be put in the same chunk (like a name and a value) * @param consumer consumer to feed each chunk * @throws VcsException */ public static void foreachChunk(@NotNull List<String> arguments, int groupSize, @NotNull ThrowableConsumer<List<String>, VcsException> consumer) throws VcsException { List<List<String>> chunks = chunkArguments(arguments, groupSize); for (List<String> chunk : chunks) { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null) indicator.checkCanceled(); consumer.consume(chunk); } } /** * Chunk arguments on the command line * * @param arguments the arguments to chunk * @return a list of lists of arguments */ @NotNull public static List<List<String>> chunkArguments(@NotNull List<String> arguments) { return chunkArguments(arguments, 1); } /** * Chunk arguments on the command line * * @param arguments the arguments to chunk, number of arguments should be divisible by groupSize * @param groupSize size of argument groups that should be put in the same chunk * @return a list of lists of arguments */ @NotNull public static List<List<String>> chunkArguments(@NotNull List<String> arguments, int groupSize) { assert arguments.size() % groupSize == 0 : "Arguments size should be divisible by group size"; ArrayList<List<String>> rc = new ArrayList<>(); int start = 0; int size = 0; int i = 0; for (; i < arguments.size(); i += groupSize) { int length = 0; for (int j = 0; j < groupSize; j++) { length += arguments.get(i + j).length(); } if (size + length > FILE_PATH_LIMIT) { if (start == i) { // to avoid empty chunks rc.add(arguments.subList(i, i + groupSize)); start = i + groupSize; } else { rc.add(arguments.subList(start, i)); start = i; } size = 0; } else { size += length; } } if (start != arguments.size()) { rc.add(arguments.subList(start, i)); } return rc; } /** * The chunk paths * * @param root the vcs root * @param files the file list * @return chunked relative paths */ public static List<List<String>> chunkPaths(VirtualFile root, Collection<FilePath> files) { return chunkArguments(toRelativePaths(root, files)); } /** * The chunk paths * * @param root the vcs root * @param files the file list * @return chunked relative paths */ public static List<List<String>> chunkFiles(@NotNull VirtualFile root, @NotNull Collection<VirtualFile> files) { return chunkArguments(toRelativeFiles(root, files)); } public static String getRelativeFilePath(VirtualFile file, @NotNull final VirtualFile baseDir) { return getRelativeFilePath(file.getPath(), baseDir); } public static String getRelativeFilePath(String file, @NotNull final VirtualFile baseDir) { if (SystemInfo.isWindows) { file = file.replace('\\', '/'); } final String basePath = baseDir.getPath(); if (!file.startsWith(basePath)) { return file; } else if (file.equals(basePath)) return "."; return file.substring(baseDir.getPath().length() + 1); } /** * Check if character is octal digit * * @param ch a character to test * @return true if the octal digit, false otherwise */ public static boolean isOctal(char ch) { return '0' <= ch && ch <= '7'; } /** * Get relative path * * @param root a root path * @param path a path to file (possibly deleted file) * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativePath(final VirtualFile root, FilePath path) { return relativePath(VfsUtil.virtualToIoFile(root), path.getIOFile()); } /** * Get relative path * * @param root a root path * @param path a path to file (possibly deleted file) * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativePath(final File root, FilePath path) { return relativePath(root, path.getIOFile()); } /** * Get relative path * * @param root a root path * @param file a virtual file * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativePath(final File root, VirtualFile file) { return relativePath(root, VfsUtil.virtualToIoFile(file)); } /** * Get relative path * * @param root a root file * @param file a virtual file * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativePath(final VirtualFile root, VirtualFile file) { return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file)); } /** * Get relative path * * @param root a root file * @param file a virtual file * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativeOrFullPath(final VirtualFile root, VirtualFile file) { if (root == null) { file.getPath(); } return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file)); } /** * Get relative path * * @param root a root path * @param path a path to file (possibly deleted file) * @return a relative path * @throws IllegalArgumentException if path is not under root. */ public static String relativePath(final File root, File path) { String rc = FileUtil.getRelativePath(root, path); if (rc == null) { throw new IllegalArgumentException("The file " + path + " cannot be made relative to " + root); } return rc.replace(File.separatorChar, '/'); } /** * Covert list of files to relative paths * * @param root a vcs root * @param filePaths a parameters to convert * @return a list of relative paths * @throws IllegalArgumentException if some path is not under root. */ public static List<String> toRelativePaths(@NotNull VirtualFile root, @NotNull final Collection<FilePath> filePaths) { ArrayList<String> rc = new ArrayList<>(filePaths.size()); for (FilePath path : filePaths) { rc.add(relativePath(root, path)); } return rc; } /** * Covert list of files to relative paths * * @param root a vcs root * @param files a parameters to convert * @return a list of relative paths * @throws IllegalArgumentException if some path is not under root. */ public static List<String> toRelativeFiles(@NotNull VirtualFile root, @NotNull final Collection<VirtualFile> files) { ArrayList<String> rc = new ArrayList<>(files.size()); for (VirtualFile file : files) { rc.add(relativePath(root, file)); } return rc; } public static void markFilesDirty(@NotNull Project project, @NotNull Collection<VirtualFile> affectedFiles) { final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project); for (VirtualFile file : affectedFiles) { if (file.isDirectory()) { dirty.dirDirtyRecursively(file); } else { dirty.fileDirty(file); } } } public static void markFilesDirty(@NotNull Project project, @NotNull List<FilePath> affectedFiles) { final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project); for (FilePath file : affectedFiles) { if (file.isDirectory()) { dirty.dirDirtyRecursively(file); } else { dirty.fileDirty(file); } } } /** * The get the possible base for the path. It tries to find the parent for the provided path. * * @param file the file to get base for * @param path the path to to check * @return the file base */ @Nullable public static VirtualFile getPossibleBase(final VirtualFile file, final String... path) { if (file == null || path.length == 0) return null; VirtualFile current = file; final List<VirtualFile> backTrace = new ArrayList<>(); int idx = path.length - 1; while (current != null) { if (SystemInfo.isFileSystemCaseSensitive ? current.getName().equals(path[idx]) : current.getName().equalsIgnoreCase(path[idx])) { if (idx == 0) { return current; } --idx; } else if (idx != path.length - 1) { int diff = path.length - 1 - idx - 1; for (int i = 0; i < diff; i++) { current = backTrace.remove(backTrace.size() - 1); } idx = path.length - 1; continue; } backTrace.add(current); current = current.getParent(); } return null; } public static void addFilesToVcsWithConfirmation(@NotNull Project project, VirtualFile... virtualFiles) { addFilesToVcsWithConfirmation(project, Arrays.asList(virtualFiles)); } /** * Finds all VCSs related to the passed files, suggests user to add files to the respected VCSs honoring addition and silence settings * and adds them if user or settings confirmed addition. Because of potentially long operation of collecting files it's highly recommended * to invoke it on the pooled thread. Method works synchronously. * * @param project project we work in * @param virtualFiles collection of virtual files to add; directories being added recursively */ public static void addFilesToVcsWithConfirmation(@NotNull Project project, @NotNull Collection<VirtualFile> virtualFiles) { if (virtualFiles.isEmpty()) { return; } ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(project); Multimap<AbstractVcs, VirtualFile> vcsMap = ArrayListMultimap.create(); for (VirtualFile createdFile : virtualFiles) { AbstractVcs vcs = vcsManager.getVcsFor(createdFile); if (vcs == null) { continue; } VfsUtil.processFileRecursivelyWithoutIgnored(createdFile, (virtualFile) -> vcsMap.put(vcs, virtualFile)); } for (AbstractVcs vcs : vcsMap.keySet()) { VcsShowConfirmationOption addOption = vcsManager.getStandardConfirmation(VcsConfiguration.StandardConfirmation.ADD, vcs); if (addOption.getValue() == VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) return; List<VirtualFile> filesList = new ArrayList<>(vcsMap.get(vcs)); if (addOption.getValue() == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY) { performAdditions(vcs, filesList); } else { AbstractVcsHelper helper = AbstractVcsHelper.getInstance(project); Ref<Collection<VirtualFile>> filesToAdd = Ref.create(); ApplicationManager.getApplication().invokeAndWait(() -> filesToAdd.set( helper .selectFilesToProcess( new ArrayList<>(filesList), VcsBundle.message("confirmation.title.add.files.to", vcs.getDisplayName()), null, VcsBundle.message("confirmation.title.add.file.to", vcs.getDisplayName()), null, addOption)) ); if (!filesToAdd.isNull()) { performAdditions(vcs, new ArrayList<>(filesToAdd.get())); } } } } private static void performAdditions(@NotNull AbstractVcs vcs, @NotNull List<VirtualFile> value) { CheckinEnvironment checkinEnvironment = vcs.getCheckinEnvironment(); if (checkinEnvironment != null) { checkinEnvironment.scheduleUnversionedFilesForAddition(value); } } }