Git: set silent for "log" in get committed changes
[idea/community.git] / plugins / git4idea / src / git4idea / GitUtil.java
1 /*
2  * Copyright 2000-2009 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 git4idea;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.intellij.openapi.util.io.FileUtil;
22 import com.intellij.openapi.vcs.FilePath;
23 import com.intellij.openapi.vcs.VcsException;
24 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
25 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
26 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
27 import com.intellij.openapi.vfs.LocalFileSystem;
28 import com.intellij.openapi.vfs.VfsUtil;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.util.Consumer;
31 import com.intellij.vcsUtil.VcsUtil;
32 import git4idea.changes.GitChangeUtils;
33 import git4idea.commands.GitCommand;
34 import git4idea.commands.GitSimpleHandler;
35 import git4idea.commands.StringScanner;
36 import git4idea.config.GitConfigUtil;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.io.File;
41 import java.io.UnsupportedEncodingException;
42 import java.nio.charset.Charset;
43 import java.util.*;
44
45 /**
46  * Git utility/helper methods
47  */
48 public class GitUtil {
49   /**
50    * The logger instance
51    */
52   private final static Logger LOG = Logger.getInstance("#git4idea.GitUtil");
53   /**
54    * Comparator for virtual files by name
55    */
56   public static final Comparator<VirtualFile> VIRTUAL_FILE_COMPARATOR = new Comparator<VirtualFile>() {
57     public int compare(final VirtualFile o1, final VirtualFile o2) {
58       if (o1 == null && o2 == null) {
59         return 0;
60       }
61       if (o1 == null) {
62         return -1;
63       }
64       if (o2 == null) {
65         return 1;
66       }
67       return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
68     }
69   };
70   /**
71    * The UTF-8 encoding name
72    */
73   public static final String UTF8_ENCODING = "UTF-8";
74   /**
75    * The UTF8 charset
76    */
77   public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
78
79   /**
80    * A private constructor to suppress instance creation
81    */
82   private GitUtil() {
83     // do nothing
84   }
85
86   /**
87    * Sort files by Git root
88    *
89    * @param virtualFiles files to sort
90    * @return sorted files
91    * @throws VcsException if non git files are passed
92    */
93   @NotNull
94   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
95     return sortFilesByGitRoot(virtualFiles, false);
96   }
97
98   /**
99    * Sort files by Git root
100    *
101    * @param virtualFiles files to sort
102    * @param ignoreNonGit if true, non-git files are ignored
103    * @return sorted files
104    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
105    */
106   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
107     throws VcsException {
108     Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
109     for (VirtualFile file : virtualFiles) {
110       final VirtualFile vcsRoot = gitRootOrNull(file);
111       if (vcsRoot == null) {
112         if (ignoreNonGit) {
113           continue;
114         }
115         else {
116           throw new VcsException("The file " + file.getPath() + " is not under Git");
117         }
118       }
119       List<VirtualFile> files = result.get(vcsRoot);
120       if (files == null) {
121         files = new ArrayList<VirtualFile>();
122         result.put(vcsRoot, files);
123       }
124       files.add(file);
125     }
126     return result;
127   }
128
129   public static String getRelativeFilePath(VirtualFile file, @NotNull final VirtualFile baseDir) {
130     return getRelativeFilePath(file.getPath(), baseDir);
131   }
132
133   public static String getRelativeFilePath(FilePath file, @NotNull final VirtualFile baseDir) {
134     return getRelativeFilePath(file.getPath(), baseDir);
135   }
136
137   public static String getRelativeFilePath(String file, @NotNull final VirtualFile baseDir) {
138     if (SystemInfo.isWindows) {
139       file = file.replace('\\', '/');
140     }
141     final String basePath = baseDir.getPath();
142     if (!file.startsWith(basePath)) {
143       return file;
144     }
145     else if (file.equals(basePath)) return ".";
146     return file.substring(baseDir.getPath().length() + 1);
147   }
148
149   /**
150    * Sort files by vcs root
151    *
152    * @param files files to sort.
153    * @return the map from root to the files under the root
154    * @throws VcsException if non git files are passed
155    */
156   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
157     return sortFilePathsByGitRoot(files, false);
158   }
159
160   /**
161    * Sort files by vcs root
162    *
163    * @param files        files to sort.
164    * @param ignoreNonGit if true, non-git files are ignored
165    * @return the map from root to the files under the root
166    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
167    */
168   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(Collection<FilePath> files, boolean ignoreNonGit)
169     throws VcsException {
170     Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
171     for (FilePath p : files) {
172       VirtualFile root = getGitRootOrNull(p);
173       if (root == null) {
174         if (ignoreNonGit) {
175           continue;
176         }
177         else {
178           throw new VcsException("The file " + p.getPath() + " is not under Git");
179         }
180       }
181       List<FilePath> l = rc.get(root);
182       if (l == null) {
183         l = new ArrayList<FilePath>();
184         rc.put(root, l);
185       }
186       l.add(p);
187     }
188     return rc;
189   }
190
191   /**
192    * Unescape path returned by the Git
193    *
194    * @param path a path to unescape
195    * @return unescaped path
196    * @throws VcsException if the path in invalid
197    */
198   public static String unescapePath(String path) throws VcsException {
199     final int l = path.length();
200     StringBuilder rc = new StringBuilder(l);
201     for (int i = 0; i < path.length(); i++) {
202       char c = path.charAt(i);
203       if (c == '\\') {
204         //noinspection AssignmentToForLoopParameter
205         i++;
206         if (i >= l) {
207           throw new VcsException("Unterminated escape sequence in the path: " + path);
208         }
209         final char e = path.charAt(i);
210         switch (e) {
211           case '\\':
212             rc.append('\\');
213             break;
214           case 't':
215             rc.append('\t');
216             break;
217           case 'n':
218             rc.append('\n');
219             break;
220           default:
221             if (isOctal(e)) {
222               // collect sequence of characters as a byte array.
223               // count bytes first
224               int n = 0;
225               for (int j = i; j < l;) {
226                 if (isOctal(path.charAt(j))) {
227                   n++;
228                   for (int k = 0; k < 3 && j < l && isOctal(path.charAt(j)); k++) {
229                     //noinspection AssignmentToForLoopParameter
230                     j++;
231                   }
232                 }
233                 if (j + 1 >= l || path.charAt(j) != '\\' || !isOctal(path.charAt(j + 1))) {
234                   break;
235                 }
236                 //noinspection AssignmentToForLoopParameter
237                 j++;
238               }
239               // convert to byte array
240               byte[] b = new byte[n];
241               n = 0;
242               while (i < l) {
243                 if (isOctal(path.charAt(i))) {
244                   int code = 0;
245                   for (int k = 0; k < 3 && i < l && isOctal(path.charAt(i)); k++) {
246                     code = code * 8 + (path.charAt(i) - '0');
247                     //noinspection AssignmentToForLoopParameter
248                     i++;
249                   }
250                   b[n++] = (byte)code;
251                 }
252                 if (i + 1 >= l || path.charAt(i) != '\\' || !isOctal(path.charAt(i + 1))) {
253                   break;
254                 }
255                 //noinspection AssignmentToForLoopParameter
256                 i++;
257               }
258               assert n == b.length;
259               // add them to string
260               final String encoding = GitConfigUtil.getFileNameEncoding();
261               try {
262                 rc.append(new String(b, encoding));
263               }
264               catch (UnsupportedEncodingException e1) {
265                 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
266               }
267             }
268             else {
269               throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
270             }
271         }
272       }
273       else {
274         rc.append(c);
275       }
276     }
277     return rc.toString();
278   }
279
280   /**
281    * Check if character is octal digit
282    *
283    * @param ch a character to test
284    * @return true if the octal digit, false otherwise
285    */
286   private static boolean isOctal(char ch) {
287     return '0' <= ch && ch <= '7';
288   }
289
290   /**
291    * Parse UNIX timestamp as it is returned by the git
292    *
293    * @param value a value to parse
294    * @return timestamp as {@link Date} object
295    */
296   public static Date parseTimestamp(String value) {
297     return new Date(Long.parseLong(value.trim()) * 1000);
298   }
299
300   /**
301    * Get git roots from content roots
302    *
303    * @param roots git content roots
304    * @return a content root
305    */
306   public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
307     HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
308     for (VirtualFile root : roots) {
309       VirtualFile f = root;
310       do {
311         if (f.findFileByRelativePath(".git") != null) {
312           rc.add(f);
313           break;
314         }
315         f = f.getParent();
316       }
317       while (f != null);
318     }
319     return rc;
320   }
321
322   /**
323    * Return a git root for the file path (the parent directory with ".git" subdirectory)
324    *
325    * @param filePath a file path
326    * @return git root for the file
327    * @throws IllegalArgumentException if the file is not under git
328    * @throws VcsException             if the file is not under git
329    */
330   public static VirtualFile getGitRoot(final FilePath filePath) throws VcsException {
331     VirtualFile root = getGitRootOrNull(filePath);
332     if (root != null) {
333       return root;
334     }
335     throw new VcsException("The file " + filePath + " is not under git.");
336   }
337
338   /**
339    * Return a git root for the file path (the parent directory with ".git" subdirectory)
340    *
341    * @param filePath a file path
342    * @return git root for the file or null if the file is not under git
343    */
344   @Nullable
345   public static VirtualFile getGitRootOrNull(final FilePath filePath) {
346     File file = filePath.getIOFile();
347     while (file != null && (!file.exists() || !file.isDirectory() || !new File(file, ".git").exists())) {
348       file = file.getParentFile();
349     }
350     if (file == null) {
351       return null;
352     }
353     return LocalFileSystem.getInstance().findFileByIoFile(file);
354   }
355
356   /**
357    * Return a git root for the file (the parent directory with ".git" subdirectory)
358    *
359    * @param file the file to check
360    * @return git root for the file
361    * @throws VcsException if the file is not under git
362    */
363   public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
364     final VirtualFile root = gitRootOrNull(file);
365     if (root != null) {
366       return root;
367     }
368     else {
369       throw new VcsException("The file " + file.getPath() + " is not under git.");
370     }
371   }
372
373   /**
374    * Return a git root for the file (the parent directory with ".git" subdirectory)
375    *
376    * @param file the file to check
377    * @return git root for the file or null if the file is not not under Git
378    */
379   @Nullable
380   public static VirtualFile gitRootOrNull(final VirtualFile file) {
381     if (file instanceof AbstractVcsVirtualFile) {
382       return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
383     }
384     VirtualFile root = file;
385     while (root != null) {
386       if (root.findFileByRelativePath(".git") != null) {
387         return root;
388       }
389       root = root.getParent();
390     }
391     return root;
392   }
393
394
395   /**
396    * Check if the virtual file under git
397    *
398    * @param vFile a virtual file
399    * @return true if the file is under git
400    */
401   public static boolean isUnderGit(final VirtualFile vFile) {
402     return gitRootOrNull(vFile) != null;
403   }
404
405   /**
406    * Get relative path
407    *
408    * @param root a root path
409    * @param path a path to file (possibly deleted file)
410    * @return a relative path
411    * @throws IllegalArgumentException if path is not under root.
412    */
413   public static String relativePath(final VirtualFile root, FilePath path) {
414     return relativePath(VfsUtil.virtualToIoFile(root), path.getIOFile());
415   }
416
417
418   /**
419    * Get relative path
420    *
421    * @param root a root path
422    * @param path a path to file (possibly deleted file)
423    * @return a relative path
424    * @throws IllegalArgumentException if path is not under root.
425    */
426   public static String relativePath(final File root, FilePath path) {
427     return relativePath(root, path.getIOFile());
428   }
429
430   /**
431    * Get relative path
432    *
433    * @param root a root path
434    * @param file a virtual file
435    * @return a relative path
436    * @throws IllegalArgumentException if path is not under root.
437    */
438   public static String relativePath(final File root, VirtualFile file) {
439     return relativePath(root, VfsUtil.virtualToIoFile(file));
440   }
441
442   /**
443    * Get relative path
444    *
445    * @param root a root file
446    * @param file a virtual file
447    * @return a relative path
448    * @throws IllegalArgumentException if path is not under root.
449    */
450   public static String relativePath(final VirtualFile root, VirtualFile file) {
451     return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
452   }
453
454   /**
455    * Get relative path
456    *
457    * @param root a root path
458    * @param path a path to file (possibly deleted file)
459    * @return a relative path
460    * @throws IllegalArgumentException if path is not under root.
461    */
462   public static String relativePath(final File root, File path) {
463     String rc = FileUtil.getRelativePath(root, path);
464     if (rc == null) {
465       throw new IllegalArgumentException("The file " + path + " cannot be made relative to " + root);
466     }
467     return rc.replace(File.separatorChar, '/');
468   }
469
470   /**
471    * Refresh files
472    *
473    * @param project       a project
474    * @param affectedFiles affected files and directories
475    */
476   public static void refreshFiles(@NotNull final Project project, @NotNull final Collection<VirtualFile> affectedFiles) {
477     final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
478     for (VirtualFile file : affectedFiles) {
479       if (!file.isValid()) {
480         continue;
481       }
482       file.refresh(false, true);
483       if (file.isDirectory()) {
484         dirty.dirDirtyRecursively(file);
485       }
486       else {
487         dirty.fileDirty(file);
488       }
489     }
490   }
491
492   /**
493    * Refresh files
494    *
495    * @param project       a project
496    * @param affectedFiles affected files and directories
497    */
498   public static void markFilesDirty(@NotNull final Project project, @NotNull final Collection<VirtualFile> affectedFiles) {
499     final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
500     for (VirtualFile file : affectedFiles) {
501       if (!file.isValid()) {
502         continue;
503       }
504       if (file.isDirectory()) {
505         dirty.dirDirtyRecursively(file);
506       }
507       else {
508         dirty.fileDirty(file);
509       }
510     }
511   }
512
513
514   /**
515    * Mark files dirty
516    *
517    * @param project       a project
518    * @param affectedFiles affected files and directories
519    */
520   public static void markFilesDirty(Project project, List<FilePath> affectedFiles) {
521     final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
522     for (FilePath file : affectedFiles) {
523       if (file.isDirectory()) {
524         dirty.dirDirtyRecursively(file);
525       }
526       else {
527         dirty.fileDirty(file);
528       }
529     }
530   }
531
532   /**
533    * Refresh files
534    *
535    * @param project       a project
536    * @param affectedFiles affected files and directories
537    */
538   public static void refreshFiles(Project project, List<FilePath> affectedFiles) {
539     final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
540     for (FilePath file : affectedFiles) {
541       VirtualFile vFile = VcsUtil.getVirtualFile(file.getIOFile());
542       if (vFile != null) {
543         vFile.refresh(false, true);
544       }
545       if (file.isDirectory()) {
546         dirty.dirDirtyRecursively(file);
547       }
548       else {
549         dirty.fileDirty(file);
550       }
551     }
552   }
553
554   /**
555    * Return committer name based on author name and committer name
556    *
557    * @param authorName    the name of author
558    * @param committerName the name of committer
559    * @return just a name if they are equal, or name that includes both author and committer
560    */
561   public static String adjustAuthorName(final String authorName, String committerName) {
562     if (!authorName.equals(committerName)) {
563       //noinspection HardCodedStringLiteral
564       committerName = authorName + ", via " + committerName;
565     }
566     return committerName;
567   }
568
569   /**
570    * Check if the file path is under git
571    *
572    * @param path the path
573    * @return true if the file path is under git
574    */
575   public static boolean isUnderGit(final FilePath path) {
576     return getGitRootOrNull(path) != null;
577   }
578
579   /**
580    * Get git roots for the selected paths
581    *
582    * @param filePaths the context paths
583    * @return a set of git roots
584    */
585   public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
586     HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
587     for (FilePath path : filePaths) {
588       final VirtualFile root = getGitRootOrNull(path);
589       if (root != null) {
590         rc.add(root);
591       }
592     }
593     return rc;
594   }
595
596   /**
597    * Get git time (UNIX time) basing on the date object
598    *
599    * @param time the time to convert
600    * @return the time in git format
601    */
602   public static String gitTime(Date time) {
603     long t = time.getTime() / 1000;
604     return Long.toString(t);
605   }
606
607   /**
608    * Format revision number from long to 16-digit abbreviated revision
609    *
610    * @param rev the abbreviated revision number as long
611    * @return the revision string
612    */
613   public static String formatLongRev(long rev) {
614     return String.format("%015x%x", (rev >>> 4), rev & 0xF);
615   }
616
617   /**
618    * The get the possible base for the path. It tries to find the parent for the provided path, if it fails, it looks for the path without last member.
619    *
620    * @param file the file to get base for
621    * @param path the path to to check
622    * @return the file base
623    */
624   @Nullable
625   public static VirtualFile getPossibleBase(VirtualFile file, String... path) {
626     return getPossibleBase(file, path.length, path);
627   }
628
629   /**
630    * The get the possible base for the path. It tries to find the parent for the provided path, if it fails, it looks for the path without last member.
631    *
632    * @param file the file to get base for
633    * @param n    the length of the path to check
634    * @param path the path to to check
635    * @return the file base
636    */
637   @Nullable
638   private static VirtualFile getPossibleBase(VirtualFile file, int n, String... path) {
639     if (file == null || n <= 0 || n > path.length) {
640       return null;
641     }
642     int i = 1;
643     VirtualFile c = file;
644     for (; c != null && i < n; i++, c = c.getParent()) {
645       if (!path[n - i].equals(c.getName())) {
646         break;
647       }
648     }
649     if (i == n && c != null) {
650       // all components matched
651       return c.getParent();
652     }
653     // try shorter paths paths
654     return getPossibleBase(file, n - 1, path);
655   }
656
657   public static void getLocalCommittedChanges(final Project project,
658                                                                    final VirtualFile root,
659                                                                    final Consumer<GitSimpleHandler> parametersSpecifier,
660                                                                    final Consumer<CommittedChangeList> consumer)
661     throws VcsException {
662     final List<CommittedChangeList> rc = new ArrayList<CommittedChangeList>();
663
664     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
665     h.setSilent(true);
666     h.setNoSSH(true);
667     h.addParameters("--pretty=format:%x0C%n" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
668     parametersSpecifier.consume(h);
669
670     String output = h.run();
671     LOG.debug("getLocalCommittedChanges output: '" + output + "'");
672     StringScanner s = new StringScanner(output);
673     while (s.hasMoreData() && s.startsWith('\u000C')) {
674       s.nextLine();
675       consumer.consume(GitChangeUtils.parseChangeList(project, root, s));
676     }
677     if (s.hasMoreData()) {
678       throw new IllegalStateException("More input is avaialble: " + s.line());
679     }
680   }
681
682   public static List<CommittedChangeList> getLocalCommittedChanges(final Project project,
683                                                                    final VirtualFile root,
684                                                                    final Consumer<GitSimpleHandler> parametersSpecifier)
685     throws VcsException {
686     final List<CommittedChangeList> rc = new ArrayList<CommittedChangeList>();
687
688     getLocalCommittedChanges(project, root, parametersSpecifier, new Consumer<CommittedChangeList>() {
689       public void consume(CommittedChangeList committedChangeList) {
690         rc.add(committedChangeList);
691       }
692     });
693
694     return rc;
695   }
696
697   /**
698    * Cast or wrap exception into a vcs exception, errors and runtime exceptions are just thrown throw.
699    *
700    * @param t an exception to throw
701    * @return a wrapped exception
702    */
703   public static VcsException rethrowVcsException(Throwable t) {
704     if (t instanceof Error) {
705       throw (Error)t;
706     }
707     if (t instanceof RuntimeException) {
708       throw (RuntimeException)t;
709     }
710     if (t instanceof VcsException) {
711       return (VcsException)t;
712     }
713     return new VcsException(t.getMessage(), t);
714   }
715 }