2 * Copyright 2000-2016 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.vcsUtil;
18 import com.intellij.ide.util.PropertiesComponent;
19 import com.intellij.openapi.actionSystem.AnActionEvent;
20 import com.intellij.openapi.actionSystem.CommonDataKeys;
21 import com.intellij.openapi.application.ReadAction;
22 import com.intellij.openapi.components.ServiceManager;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.fileEditor.FileDocumentManager;
25 import com.intellij.openapi.fileTypes.FileTypeManager;
26 import com.intellij.openapi.fileTypes.FileTypes;
27 import com.intellij.openapi.progress.ProgressManager;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.util.Comparing;
30 import com.intellij.openapi.util.Ref;
31 import com.intellij.openapi.util.io.FileUtil;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.openapi.vcs.*;
34 import com.intellij.openapi.vcs.actions.VcsContextFactory;
35 import com.intellij.openapi.vcs.changes.Change;
36 import com.intellij.openapi.vcs.changes.ContentRevision;
37 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
38 import com.intellij.openapi.vcs.roots.VcsRootDetector;
39 import com.intellij.openapi.vfs.*;
40 import com.intellij.openapi.vfs.newvfs.RefreshQueue;
41 import com.intellij.openapi.wm.StatusBar;
42 import com.intellij.util.Function;
43 import com.intellij.util.containers.ContainerUtilRt;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
49 import java.io.IOException;
52 import static com.intellij.openapi.application.ApplicationManager.getApplication;
53 import static com.intellij.util.ObjectUtils.notNull;
54 import static java.util.stream.Collectors.groupingBy;
56 @SuppressWarnings({"UtilityClassWithoutPrivateConstructor"})
57 public class VcsUtil {
58 protected static final char[] ourCharsToBeChopped = new char[]{'/', '\\'};
59 private static final Logger LOG = Logger.getInstance("#com.intellij.vcsUtil.VcsUtil");
61 public final static String MAX_VCS_LOADED_SIZE_KB = "idea.max.vcs.loaded.size.kb";
62 private static final int ourMaxLoadedFileSize = computeLoadedFileSize();
64 @NotNull private static final VcsRoot FICTIVE_ROOT = new VcsRoot(null, null);
66 public static int getMaxVcsLoadedFileSize() {
67 return ourMaxLoadedFileSize;
70 private static int computeLoadedFileSize() {
71 int result = (int)PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD;
72 String userLimitKb = System.getProperty(MAX_VCS_LOADED_SIZE_KB);
74 return userLimitKb != null ? Integer.parseInt(userLimitKb) * 1024 : result;
76 catch (NumberFormatException ignored) {
81 public static void markFileAsDirty(final Project project, final VirtualFile file) {
82 VcsDirtyScopeManager.getInstance(project).fileDirty(file);
85 public static void markFileAsDirty(final Project project, final FilePath path) {
86 VcsDirtyScopeManager.getInstance(project).fileDirty(path);
89 public static void markFileAsDirty(final Project project, final String path) {
90 final FilePath filePath = VcsContextFactory.SERVICE.getInstance().createFilePathOn(new File(path));
91 markFileAsDirty( project, filePath );
94 public static void refreshFiles(Project project, HashSet<FilePath> paths) {
95 for (FilePath path : paths) {
96 VirtualFile vFile = path.getVirtualFile();
98 if (vFile.isDirectory()) {
99 markFileAsDirty(project, vFile);
102 vFile.refresh(true, vFile.isDirectory());
109 * @param project Project component
110 * @param file File to check
111 * @return true if the given file resides under the root associated with any
113 public static boolean isFileUnderVcs(Project project, String file) {
114 return getVcsFor(project, getFilePath(file)) != null;
117 public static boolean isFileUnderVcs(Project project, FilePath file) {
118 return getVcsFor(project, file) != null;
122 * File is considered to be a valid vcs file if it resides under the content
123 * root controlled by the given vcs.
125 public static boolean isFileForVcs(@NotNull VirtualFile file, Project project, AbstractVcs host) {
126 return getVcsFor(project, file) == host;
129 // NB: do not reduce this method to the method above since PLVcsMgr uses
130 // different methods for computing its predicate (since FilePath can
131 // refer to the deleted files).
132 public static boolean isFileForVcs(FilePath path, Project project, AbstractVcs host) {
133 return getVcsFor(project, path) == host;
136 public static boolean isFileForVcs(String path, Project project, AbstractVcs host) {
137 return getVcsFor(project, getFilePath(path)) == host;
141 public static AbstractVcs getVcsFor(@NotNull Project project, FilePath filePath) {
142 return computeValue(project, manager -> manager.getVcsFor(filePath));
146 public static AbstractVcs getVcsFor(@NotNull Project project, @NotNull VirtualFile file) {
147 return computeValue(project, manager -> manager.getVcsFor(file));
151 public static VirtualFile getVcsRootFor(@NotNull Project project, FilePath filePath) {
152 return computeValue(project, manager -> manager.getVcsRootFor(filePath));
156 public static VirtualFile getVcsRootFor(@NotNull Project project, @Nullable VirtualFile file) {
157 return computeValue(project, manager -> manager.getVcsRootFor(file));
161 private static <T> T computeValue(@NotNull Project project, @NotNull Function<ProjectLevelVcsManager, T> provider) {
162 return ReadAction.compute(() -> {
163 // IDEADEV-17916, when e.g. ContentRevision.getContent is called in
164 // a future task after the component has been disposed.
166 if (!project.isDisposed()) {
167 ProjectLevelVcsManager manager = ProjectLevelVcsManager.getInstance(project);
168 result = manager != null ? provider.fun(manager) : null;
174 public static void refreshFiles(final FilePath[] roots, final Runnable runnable) {
175 getApplication().assertIsDispatchThread();
176 refreshFiles(collectFilesToRefresh(roots), runnable);
179 public static void refreshFiles(final File[] roots, final Runnable runnable) {
180 getApplication().assertIsDispatchThread();
181 refreshFiles(collectFilesToRefresh(roots), runnable);
184 private static File[] collectFilesToRefresh(final FilePath[] roots) {
185 final File[] result = new File[roots.length];
186 for (int i = 0; i < roots.length; i++) {
187 result[i] = roots[i].getIOFile();
192 private static void refreshFiles(final List<VirtualFile> filesToRefresh, final Runnable runnable) {
193 RefreshQueue.getInstance().refresh(true, true, runnable, filesToRefresh);
196 private static List<VirtualFile> collectFilesToRefresh(final File[] roots) {
197 final ArrayList<VirtualFile> result = new ArrayList<>();
198 for (File root : roots) {
199 VirtualFile vFile = findFileFor(root);
203 LOG.info("Failed to find VirtualFile for one of refresh roots: " + root.getAbsolutePath());
210 private static VirtualFile findFileFor(final File root) {
212 while (current != null) {
213 final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(root);
214 if (vFile != null) return vFile;
215 current = current.getParentFile();
222 public static VirtualFile getVirtualFile(@NotNull String path) {
223 return ReadAction.compute(() -> LocalFileSystem.getInstance().findFileByPath(path.replace(File.separatorChar, '/')));
227 public static VirtualFile getVirtualFile(@NotNull File file) {
228 return ReadAction.compute(() -> LocalFileSystem.getInstance().findFileByIoFile(file));
232 public static VirtualFile getVirtualFileWithRefresh(final File file) {
233 if (file == null) return null;
234 final LocalFileSystem lfs = LocalFileSystem.getInstance();
235 VirtualFile result = lfs.findFileByIoFile(file);
236 if (result == null) {
237 result = lfs.refreshAndFindFileByIoFile(file);
242 public static String getFileContent(@NotNull String path) {
243 return ReadAction.compute(() -> {
244 VirtualFile vFile = getVirtualFile(path);
245 assert vFile != null;
246 return FileDocumentManager.getInstance().getDocument(vFile).getText();
251 public static byte[] getFileByteContent(@NotNull File file) {
253 return FileUtil.loadFileBytes(file);
255 catch (IOException e) {
261 public static FilePath getFilePath(String path) {
262 return getFilePath(new File(path));
265 public static FilePath getFilePath(@NotNull VirtualFile file) {
266 return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file);
269 public static FilePath getFilePath(@NotNull File file) {
270 return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file);
273 public static FilePath getFilePath(@NotNull String path, boolean isDirectory) {
274 return VcsContextFactory.SERVICE.getInstance().createFilePath(path, isDirectory);
277 public static FilePath getFilePathOnNonLocal(String path, boolean isDirectory) {
278 return VcsContextFactory.SERVICE.getInstance().createFilePathOnNonLocal(path, isDirectory);
281 public static FilePath getFilePath(@NotNull File file, boolean isDirectory) {
282 return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file, isDirectory);
285 public static FilePath getFilePathForDeletedFile(@NotNull String path, boolean isDirectory) {
286 return VcsContextFactory.SERVICE.getInstance().createFilePathOnDeleted(new File(path), isDirectory);
290 public static FilePath getFilePath(@NotNull VirtualFile parent, @NotNull String name) {
291 return VcsContextFactory.SERVICE.getInstance().createFilePathOn(parent, name);
295 public static FilePath getFilePath(@NotNull VirtualFile parent, @NotNull String fileName, boolean isDirectory) {
296 return VcsContextFactory.SERVICE.getInstance().createFilePath(parent, fileName, isDirectory);
300 * Shows message in the status bar.
302 * @param project Current project component
303 * @param message information message
305 public static void showStatusMessage(@NotNull Project project, @Nullable String message) {
306 SwingUtilities.invokeLater(() -> {
307 if (project.isOpen()) {
308 StatusBar.Info.set(message, project);
314 * @param change "Change" description.
315 * @return Return true if the "Change" object is created for "Rename" operation:
316 * in this case name of files for "before" and "after" revisions must not
319 public static boolean isRenameChange(Change change) {
320 boolean isRenamed = false;
321 ContentRevision before = change.getBeforeRevision();
322 ContentRevision after = change.getAfterRevision();
323 if (before != null && after != null) {
324 String prevFile = getCanonicalLocalPath(before.getFile().getPath());
325 String newFile = getCanonicalLocalPath(after.getFile().getPath());
326 isRenamed = !prevFile.equals(newFile);
332 * @param change "Change" description.
333 * @return Return true if the "Change" object is created for "New" operation:
334 * "before" revision is obviously NULL, while "after" revision is not.
336 public static boolean isChangeForNew(Change change) {
337 return (change.getBeforeRevision() == null) && (change.getAfterRevision() != null);
341 * @param change "Change" description.
342 * @return Return true if the "Change" object is created for "Delete" operation:
343 * "before" revision is NOT NULL, while "after" revision is NULL.
345 public static boolean isChangeForDeleted(Change change) {
346 return (change.getBeforeRevision() != null) && (change.getAfterRevision() == null);
349 public static boolean isChangeForFolder(Change change) {
350 ContentRevision revB = change.getBeforeRevision();
351 ContentRevision revA = change.getAfterRevision();
352 return (revA != null && revA.getFile().isDirectory()) || (revB != null && revB.getFile().isDirectory());
356 * Sort file paths so that paths under the same root are placed from the
357 * innermost to the outermost (closest to the root).
359 * @param files An array of file paths to be sorted. Sorting is done over the parameter.
360 * @return Sorted array of the file paths.
362 public static FilePath[] sortPathsFromInnermost(FilePath[] files) {
363 return sortPaths(files, -1);
367 * Sort file paths so that paths under the same root are placed from the
368 * outermost to the innermost (farest from the root).
370 * @param files An array of file paths to be sorted. Sorting is done over the parameter.
371 * @return Sorted array of the file paths.
373 public static FilePath[] sortPathsFromOutermost(FilePath[] files) {
374 return sortPaths(files, 1);
377 private static FilePath[] sortPaths(FilePath[] files, final int sign) {
378 Arrays.sort(files, (file1, file2) -> sign * file1.getPath().compareTo(file2.getPath()));
383 * @param e ActionEvent object
384 * @return <code>VirtualFile</code> available in the current context.
385 * Returns not <code>null</code> if and only if exectly one file is available.
388 public static VirtualFile getOneVirtualFile(AnActionEvent e) {
389 VirtualFile[] files = getVirtualFiles(e);
390 return (files.length != 1) ? null : files[0];
394 * @param e ActionEvent object
395 * @return <code>VirtualFile</code>s available in the current context.
396 * Returns empty array if there are no available files.
398 public static VirtualFile[] getVirtualFiles(AnActionEvent e) {
399 VirtualFile[] files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY);
400 return (files == null) ? VirtualFile.EMPTY_ARRAY : files;
404 * Collects all files which are located in the passed directory.
406 * @throws IllegalArgumentException if <code>dir</code> isn't a directory.
408 public static void collectFiles(final VirtualFile dir,
409 final List<VirtualFile> files,
410 final boolean recursive,
411 final boolean addDirectories) {
412 if (!dir.isDirectory()) {
413 throw new IllegalArgumentException(VcsBundle.message("exception.text.file.should.be.directory", dir.getPresentableUrl()));
416 final FileTypeManager fileTypeManager = FileTypeManager.getInstance();
417 VfsUtilCore.visitChildrenRecursively(dir, new VirtualFileVisitor() {
419 public boolean visitFile(@NotNull VirtualFile file) {
420 if (file.isDirectory()) {
421 if (addDirectories) {
424 if (!recursive && !Comparing.equal(file, dir)) {
428 else if (fileTypeManager == null || file.getFileType() != FileTypes.UNKNOWN) {
436 public static boolean runVcsProcessWithProgress(final VcsRunnable runnable, String progressTitle, boolean canBeCanceled, Project project)
437 throws VcsException {
438 final Ref<VcsException> ex = new Ref<>();
439 boolean result = ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
443 catch (VcsException e) {
446 }, progressTitle, canBeCanceled, project);
454 public static VirtualFile waitForTheFile(@NotNull String path) {
455 Ref<VirtualFile> result = Ref.create();
457 getApplication().invokeAndWait(
458 () -> getApplication().runWriteAction(
459 () -> result.set(LocalFileSystem.getInstance().refreshAndFindFileByPath(path))));
464 public static String getCanonicalLocalPath(String localPath) {
465 localPath = chopTrailingChars(localPath.trim().replace('\\', '/'), ourCharsToBeChopped);
466 if (localPath.length() == 2 && localPath.charAt(1) == ':') {
472 public static String getCanonicalPath( String path )
475 try { canonPath = new File( path ).getCanonicalPath(); }
476 catch( IOException e ){ canonPath = path; }
480 public static String getCanonicalPath( File file )
483 try { canonPath = file.getCanonicalPath(); }
484 catch (IOException e) { canonPath = file.getAbsolutePath(); }
489 * @param source Source string
490 * @param chars Symbols to be trimmed
491 * @return string without all specified chars at the end. For example,
492 * <code>chopTrailingChars("c:\\my_directory\\//\\",new char[]{'\\'}) is <code>"c:\\my_directory\\//"</code>,
493 * <code>chopTrailingChars("c:\\my_directory\\//\\",new char[]{'\\','/'}) is <code>"c:\my_directory"</code>.
494 * Actually this method can be used to normalize file names to chop trailing separator chars.
496 public static String chopTrailingChars(String source, char[] chars) {
497 StringBuilder sb = new StringBuilder(source);
499 boolean atLeastOneCharWasChopped = false;
500 for (int i = 0; i < chars.length && sb.length() > 0; i++) {
501 if (sb.charAt(sb.length() - 1) == chars[i]) {
502 sb.deleteCharAt(sb.length() - 1);
503 atLeastOneCharWasChopped = true;
506 if (!atLeastOneCharWasChopped) {
510 return sb.toString();
513 public static VirtualFile[] paths2VFiles(String[] paths) {
514 VirtualFile[] files = new VirtualFile[paths.length];
515 for (int i = 0; i < paths.length; i++) {
516 files[i] = getVirtualFile(paths[i]);
522 private static final String ANNO_ASPECT = "show.vcs.annotation.aspect.";
523 //public static boolean isAspectAvailableByDefault(LineAnnotationAspect aspect) {
524 // if (aspect.getId() == null) return aspect.isShowByDefault();
525 // return PropertiesComponent.getInstance().getBoolean(ANNO_ASPECT + aspect.getId(), aspect.isShowByDefault());
528 public static boolean isAspectAvailableByDefault(String id) {
529 return isAspectAvailableByDefault(id, true);
532 public static boolean isAspectAvailableByDefault(@Nullable String id, boolean defaultValue) {
533 if (id == null) return false;
534 return PropertiesComponent.getInstance().getBoolean(ANNO_ASPECT + id, defaultValue);
537 public static void setAspectAvailability(String aspectID, boolean showByDefault) {
538 PropertiesComponent.getInstance().setValue(ANNO_ASPECT + aspectID, String.valueOf(showByDefault));
541 public static boolean isPathRemote(String path) {
542 final int idx = path.indexOf("://");
544 final int idx2 = path.indexOf(":\\\\");
553 public static String getPathForProgressPresentation(@NotNull final File file) {
554 return file.getName() + " (" + file.getParent() + ")";
558 public static <T> Map<VcsRoot, List<T>> groupByRoots(@NotNull Project project,
559 @NotNull Collection<T> items,
560 @NotNull Function<T, FilePath> filePathMapper) {
561 ProjectLevelVcsManager manager = ProjectLevelVcsManager.getInstance(project);
563 return items.stream().collect(groupingBy(item -> notNull(manager.getVcsRootObjectFor(filePathMapper.fun(item)), FICTIVE_ROOT)));
567 public static Collection<VcsDirectoryMapping> findRoots(@NotNull VirtualFile rootDir, @NotNull Project project)
568 throws IllegalArgumentException {
569 if (!rootDir.isDirectory()) {
570 throw new IllegalArgumentException(
571 "Can't find VCS at the target file system path. Reason: expected to find a directory there but it's not. The path: "
572 + rootDir.getParent()
575 Collection<VcsRoot> roots = ServiceManager.getService(project, VcsRootDetector.class).detect(rootDir);
576 Collection<VcsDirectoryMapping> result = ContainerUtilRt.newArrayList();
577 for (VcsRoot vcsRoot : roots) {
578 VirtualFile vFile = vcsRoot.getPath();
579 AbstractVcs rootVcs = vcsRoot.getVcs();
580 if (rootVcs != null && vFile != null) {
581 result.add(new VcsDirectoryMapping(vFile.getPath(), rootVcs.getName()));
588 public static List<VcsDirectoryMapping> addMapping(@NotNull List<VcsDirectoryMapping> existingMappings,
589 @NotNull String path,
590 @NotNull String vcs) {
591 List<VcsDirectoryMapping> mappings = new ArrayList<>(existingMappings);
592 for (Iterator<VcsDirectoryMapping> iterator = mappings.iterator(); iterator.hasNext(); ) {
593 VcsDirectoryMapping mapping = iterator.next();
594 if (mapping.isDefaultMapping() && StringUtil.isEmptyOrSpaces(mapping.getVcs())) {
595 LOG.debug("Removing <Project> -> <None> mapping");
598 else if (FileUtil.pathsEqual(mapping.getDirectory(), path)) {
599 if (!StringUtil.isEmptyOrSpaces(mapping.getVcs())) {
600 LOG.warn("Substituting existing mapping [" + path + "] -> [" + mapping.getVcs() + "] with [" + vcs + "]");
603 LOG.debug("Removing [" + path + "] -> <None> mapping");
608 mappings.add(new VcsDirectoryMapping(path, vcs));