vcs: Refactored "VcsUtil" - removed duplication, used lambdas
[idea/community.git] / platform / vcs-api / src / com / intellij / vcsUtil / VcsUtil.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.vcsUtil;
17
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;
46
47 import javax.swing.*;
48 import java.io.File;
49 import java.io.IOException;
50 import java.util.*;
51
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;
55
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");
60
61   public final static String MAX_VCS_LOADED_SIZE_KB = "idea.max.vcs.loaded.size.kb";
62   private static final int ourMaxLoadedFileSize = computeLoadedFileSize();
63
64   @NotNull private static final VcsRoot FICTIVE_ROOT = new VcsRoot(null, null);
65
66   public static int getMaxVcsLoadedFileSize() {
67     return ourMaxLoadedFileSize;
68   }
69
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);
73     try {
74       return userLimitKb != null ? Integer.parseInt(userLimitKb) * 1024 : result;
75     }
76     catch (NumberFormatException ignored) {
77       return result;
78     }
79   }
80
81   public static void markFileAsDirty(final Project project, final VirtualFile file) {
82     VcsDirtyScopeManager.getInstance(project).fileDirty(file);
83   }
84
85   public static void markFileAsDirty(final Project project, final FilePath path) {
86       VcsDirtyScopeManager.getInstance(project).fileDirty(path);
87   }
88
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 );
92   }
93
94   public static void refreshFiles(Project project, HashSet<FilePath> paths) {
95     for (FilePath path : paths) {
96       VirtualFile vFile = path.getVirtualFile();
97       if (vFile != null) {
98         if (vFile.isDirectory()) {
99           markFileAsDirty(project, vFile);
100         }
101         else {
102           vFile.refresh(true, vFile.isDirectory());
103         }
104       }
105     }
106   }
107
108   /**
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
112    */
113   public static boolean isFileUnderVcs(Project project, String file) {
114     return getVcsFor(project, getFilePath(file)) != null;
115   }
116
117   public static boolean isFileUnderVcs(Project project, FilePath file) {
118     return getVcsFor(project, file) != null;
119   }
120
121   /**
122    * File is considered to be a valid vcs file if it resides under the content
123    * root controlled by the given vcs.
124    */
125   public static boolean isFileForVcs(@NotNull VirtualFile file, Project project, AbstractVcs host) {
126     return getVcsFor(project, file) == host;
127   }
128
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;
134   }
135
136   public static boolean isFileForVcs(String path, Project project, AbstractVcs host) {
137     return getVcsFor(project, getFilePath(path)) == host;
138   }
139
140   @Nullable
141   public static AbstractVcs getVcsFor(@NotNull Project project, FilePath filePath) {
142     return computeValue(project, manager -> manager.getVcsFor(filePath));
143   }
144
145   @Nullable
146   public static AbstractVcs getVcsFor(@NotNull Project project, @NotNull VirtualFile file) {
147     return computeValue(project, manager -> manager.getVcsFor(file));
148   }
149
150   @Nullable
151   public static VirtualFile getVcsRootFor(@NotNull Project project, FilePath filePath) {
152     return computeValue(project, manager -> manager.getVcsRootFor(filePath));
153   }
154
155   @Nullable
156   public static VirtualFile getVcsRootFor(@NotNull Project project, @Nullable VirtualFile file) {
157     return computeValue(project, manager -> manager.getVcsRootFor(file));
158   }
159
160   @Nullable
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.
165       T result = null;
166       if (!project.isDisposed()) {
167         ProjectLevelVcsManager manager = ProjectLevelVcsManager.getInstance(project);
168         result = manager != null ? provider.fun(manager) : null;
169       }
170       return result;
171     });
172   }
173
174   public static void refreshFiles(final FilePath[] roots, final Runnable runnable) {
175     getApplication().assertIsDispatchThread();
176     refreshFiles(collectFilesToRefresh(roots), runnable);
177   }
178
179   public static void refreshFiles(final File[] roots, final Runnable runnable) {
180     getApplication().assertIsDispatchThread();
181     refreshFiles(collectFilesToRefresh(roots), runnable);
182   }
183
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();
188     }
189     return result;
190   }
191
192   private static void refreshFiles(final List<VirtualFile> filesToRefresh, final Runnable runnable) {
193     RefreshQueue.getInstance().refresh(true, true, runnable, filesToRefresh);
194   }
195
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);
200       if (vFile != null) {
201         result.add(vFile);
202       } else {
203         LOG.info("Failed to find VirtualFile for one of refresh roots: " + root.getAbsolutePath());
204       }
205     }
206     return result;
207   }
208
209   @Nullable
210   private static VirtualFile findFileFor(final File root) {
211     File current = root;
212     while (current != null) {
213       final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(root);
214       if (vFile != null) return vFile;
215       current = current.getParentFile();
216     }
217
218     return null;
219   }
220
221   @Nullable
222   public static VirtualFile getVirtualFile(@NotNull String path) {
223     return ReadAction.compute(() -> LocalFileSystem.getInstance().findFileByPath(path.replace(File.separatorChar, '/')));
224   }
225
226   @Nullable
227   public static VirtualFile getVirtualFile(@NotNull File file) {
228     return ReadAction.compute(() -> LocalFileSystem.getInstance().findFileByIoFile(file));
229   }
230
231   @Nullable
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);
238     }
239     return result;
240   }
241
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();
247     });
248   }
249
250   @Nullable
251   public static byte[] getFileByteContent(@NotNull File file) {
252     try {
253       return FileUtil.loadFileBytes(file);
254     }
255     catch (IOException e) {
256       LOG.info(e);
257       return null;
258     }
259   }
260
261   public static FilePath getFilePath(String path) {
262     return getFilePath(new File(path));
263   }
264
265   public static FilePath getFilePath(@NotNull VirtualFile file) {
266     return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file);
267   }
268
269   public static FilePath getFilePath(@NotNull File file) {
270     return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file);
271   }
272
273   public static FilePath getFilePath(@NotNull String path, boolean isDirectory) {
274     return VcsContextFactory.SERVICE.getInstance().createFilePath(path, isDirectory);
275   }
276
277   public static FilePath getFilePathOnNonLocal(String path, boolean isDirectory) {
278     return VcsContextFactory.SERVICE.getInstance().createFilePathOnNonLocal(path, isDirectory);
279   }
280
281   public static FilePath getFilePath(@NotNull File file, boolean isDirectory) {
282     return VcsContextFactory.SERVICE.getInstance().createFilePathOn(file, isDirectory);
283   }
284
285   public static FilePath getFilePathForDeletedFile(@NotNull String path, boolean isDirectory) {
286     return VcsContextFactory.SERVICE.getInstance().createFilePathOnDeleted(new File(path), isDirectory);
287   }
288
289   @NotNull
290   public static FilePath getFilePath(@NotNull VirtualFile parent, @NotNull String name) {
291     return VcsContextFactory.SERVICE.getInstance().createFilePathOn(parent, name);
292   }
293
294   @NotNull
295   public static FilePath getFilePath(@NotNull VirtualFile parent, @NotNull String fileName, boolean isDirectory) {
296     return VcsContextFactory.SERVICE.getInstance().createFilePath(parent, fileName, isDirectory);
297   }
298
299   /**
300    * Shows message in the status bar.
301    *
302    * @param project Current project component
303    * @param message information message
304    */
305   public static void showStatusMessage(@NotNull Project project, @Nullable String message) {
306     SwingUtilities.invokeLater(() -> {
307       if (project.isOpen()) {
308         StatusBar.Info.set(message, project);
309       }
310     });
311   }
312
313   /**
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
317    *         coniside.
318    */
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);
327     }
328     return isRenamed;
329   }
330
331   /**
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.
335    */
336   public static boolean isChangeForNew(Change change) {
337     return (change.getBeforeRevision() == null) && (change.getAfterRevision() != null);
338   }
339
340   /**
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.
344    */
345   public static boolean isChangeForDeleted(Change change) {
346     return (change.getBeforeRevision() != null) && (change.getAfterRevision() == null);
347   }
348
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());
353   }
354
355   /**
356    * Sort file paths so that paths under the same root are placed from the
357    * innermost to the outermost (closest to the root).
358    *
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.
361    */
362   public static FilePath[] sortPathsFromInnermost(FilePath[] files) {
363     return sortPaths(files, -1);
364   }
365
366   /**
367    * Sort file paths so that paths under the same root are placed from the
368    * outermost to the innermost (farest from the root).
369    *
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.
372    */
373   public static FilePath[] sortPathsFromOutermost(FilePath[] files) {
374     return sortPaths(files, 1);
375   }
376
377   private static FilePath[] sortPaths(FilePath[] files, final int sign) {
378     Arrays.sort(files, (file1, file2) -> sign * file1.getPath().compareTo(file2.getPath()));
379     return files;
380   }
381
382   /**
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.
386    */
387   @Nullable
388   public static VirtualFile getOneVirtualFile(AnActionEvent e) {
389     VirtualFile[] files = getVirtualFiles(e);
390     return (files.length != 1) ? null : files[0];
391   }
392
393   /**
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.
397    */
398   public static VirtualFile[] getVirtualFiles(AnActionEvent e) {
399     VirtualFile[] files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY);
400     return (files == null) ? VirtualFile.EMPTY_ARRAY : files;
401   }
402
403   /**
404    * Collects all files which are located in the passed directory.
405    *
406    * @throws IllegalArgumentException if <code>dir</code> isn't a directory.
407    */
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()));
414     }
415
416     final FileTypeManager fileTypeManager = FileTypeManager.getInstance();
417     VfsUtilCore.visitChildrenRecursively(dir, new VirtualFileVisitor() {
418       @Override
419       public boolean visitFile(@NotNull VirtualFile file) {
420         if (file.isDirectory()) {
421           if (addDirectories) {
422             files.add(file);
423           }
424           if (!recursive && !Comparing.equal(file, dir)) {
425             return false;
426           }
427         }
428         else if (fileTypeManager == null || file.getFileType() != FileTypes.UNKNOWN) {
429           files.add(file);
430         }
431         return true;
432       }
433     });
434   }
435
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(() -> {
440       try {
441         runnable.run();
442       }
443       catch (VcsException e) {
444         ex.set(e);
445       }
446     }, progressTitle, canBeCanceled, project);
447     if (!ex.isNull()) {
448       throw ex.get();
449     }
450     return result;
451   }
452
453   @Nullable
454   public static VirtualFile waitForTheFile(@NotNull String path) {
455     Ref<VirtualFile> result = Ref.create();
456
457     getApplication().invokeAndWait(
458       () -> getApplication().runWriteAction(
459         () -> result.set(LocalFileSystem.getInstance().refreshAndFindFileByPath(path))));
460
461     return result.get();
462   }
463
464   public static String getCanonicalLocalPath(String localPath) {
465     localPath = chopTrailingChars(localPath.trim().replace('\\', '/'), ourCharsToBeChopped);
466     if (localPath.length() == 2 && localPath.charAt(1) == ':') {
467       localPath += '/';
468     }
469     return localPath;
470   }
471
472   public static String getCanonicalPath( String path )
473   {
474     String canonPath;
475     try {  canonPath = new File( path ).getCanonicalPath();  }
476     catch( IOException e ){  canonPath = path;  }
477     return canonPath;
478   }
479
480   public static String getCanonicalPath( File file )
481   {
482     String canonPath;
483     try {  canonPath = file.getCanonicalPath();  }
484     catch (IOException e) {  canonPath = file.getAbsolutePath();  }
485     return canonPath;
486   }
487
488   /**
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.
495    */
496   public static String chopTrailingChars(String source, char[] chars) {
497     StringBuilder sb = new StringBuilder(source);
498     while (true) {
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;
504         }
505       }
506       if (!atLeastOneCharWasChopped) {
507         break;
508       }
509     }
510     return sb.toString();
511   }
512
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]);
517     }
518
519     return files;
520   }
521
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());
526   //}
527
528   public static boolean isAspectAvailableByDefault(String id) {
529     return isAspectAvailableByDefault(id, true);
530   }
531
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);
535   }
536
537   public static void setAspectAvailability(String aspectID, boolean showByDefault) {
538     PropertiesComponent.getInstance().setValue(ANNO_ASPECT + aspectID, String.valueOf(showByDefault));
539   }
540
541   public static boolean isPathRemote(String path) {
542     final int idx = path.indexOf("://");
543     if (idx == -1) {
544       final int idx2 = path.indexOf(":\\\\");
545       if (idx2 == -1) {
546         return false;
547       }
548       return idx2 > 0;
549     }
550     return idx > 0;
551   }
552
553   public static String getPathForProgressPresentation(@NotNull final File file) {
554     return file.getName() + " (" + file.getParent() + ")";
555   }
556
557   @NotNull
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);
562
563     return items.stream().collect(groupingBy(item -> notNull(manager.getVcsRootObjectFor(filePathMapper.fun(item)), FICTIVE_ROOT)));
564   }
565
566   @NotNull
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()
573       );
574     }
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()));
582       }
583     }
584     return result;
585   }
586
587   @NotNull
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");
596         iterator.remove();
597       }
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 + "]");
601         }
602         else {
603           LOG.debug("Removing [" + path + "] -> <None> mapping");
604         }
605         iterator.remove();
606       }
607     }
608     mappings.add(new VcsDirectoryMapping(path, vcs));
609     return mappings;
610   }
611 }