VCS: asynch load of committed changes [in "browse changes" - SVN, CVS] - works for...
[fedora-idea.git] / plugins / git4idea / src / git4idea / GitUtil.java
blob55ec70779737415df17ac69bac976c187350eb8b
1 /*
2 * Copyright 2000-2009 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 git4idea;
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;
40 import java.io.File;
41 import java.io.UnsupportedEncodingException;
42 import java.nio.charset.Charset;
43 import java.util.*;
45 /**
46 * Git utility/helper methods
48 public class GitUtil {
49 /**
50 * The logger instance
52 private final static Logger LOG = Logger.getInstance("#git4idea.GitUtil");
53 /**
54 * Comparator for virtual files by name
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;
61 if (o1 == null) {
62 return -1;
64 if (o2 == null) {
65 return 1;
67 return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
70 /**
71 * The UTF-8 encoding name
73 public static final String UTF8_ENCODING = "UTF-8";
74 /**
75 * The UTF8 charset
77 public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
79 /**
80 * A private constructor to suppress instance creation
82 private GitUtil() {
83 // do nothing
86 /**
87 * Sort files by Git root
89 * @param virtualFiles files to sort
90 * @return sorted files
91 * @throws VcsException if non git files are passed
93 @NotNull
94 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
95 return sortFilesByGitRoot(virtualFiles, false);
98 /**
99 * Sort files by Git root
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
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;
115 else {
116 throw new VcsException("The file " + file.getPath() + " is not under Git");
119 List<VirtualFile> files = result.get(vcsRoot);
120 if (files == null) {
121 files = new ArrayList<VirtualFile>();
122 result.put(vcsRoot, files);
124 files.add(file);
126 return result;
129 public static String getRelativeFilePath(VirtualFile file, @NotNull final VirtualFile baseDir) {
130 return getRelativeFilePath(file.getPath(), baseDir);
133 public static String getRelativeFilePath(FilePath file, @NotNull final VirtualFile baseDir) {
134 return getRelativeFilePath(file.getPath(), baseDir);
137 public static String getRelativeFilePath(String file, @NotNull final VirtualFile baseDir) {
138 if (SystemInfo.isWindows) {
139 file = file.replace('\\', '/');
141 final String basePath = baseDir.getPath();
142 if (!file.startsWith(basePath)) {
143 return file;
145 else if (file.equals(basePath)) return ".";
146 return file.substring(baseDir.getPath().length() + 1);
150 * Sort files by vcs root
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
156 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
157 return sortFilePathsByGitRoot(files, false);
161 * Sort files by vcs root
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
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;
177 else {
178 throw new VcsException("The file " + p.getPath() + " is not under Git");
181 List<FilePath> l = rc.get(root);
182 if (l == null) {
183 l = new ArrayList<FilePath>();
184 rc.put(root, l);
186 l.add(p);
188 return rc;
192 * Unescape path returned by the Git
194 * @param path a path to unescape
195 * @return unescaped path
196 * @throws VcsException if the path in invalid
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);
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++;
233 if (j + 1 >= l || path.charAt(j) != '\\' || !isOctal(path.charAt(j + 1))) {
234 break;
236 //noinspection AssignmentToForLoopParameter
237 j++;
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++;
250 b[n++] = (byte)code;
252 if (i + 1 >= l || path.charAt(i) != '\\' || !isOctal(path.charAt(i + 1))) {
253 break;
255 //noinspection AssignmentToForLoopParameter
256 i++;
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));
264 catch (UnsupportedEncodingException e1) {
265 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
268 else {
269 throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
273 else {
274 rc.append(c);
277 return rc.toString();
281 * Check if character is octal digit
283 * @param ch a character to test
284 * @return true if the octal digit, false otherwise
286 private static boolean isOctal(char ch) {
287 return '0' <= ch && ch <= '7';
291 * Parse UNIX timestamp as it is returned by the git
293 * @param value a value to parse
294 * @return timestamp as {@link Date} object
296 public static Date parseTimestamp(String value) {
297 return new Date(Long.parseLong(value.trim()) * 1000);
301 * Get git roots from content roots
303 * @param roots git content roots
304 * @return a content root
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;
315 f = f.getParent();
317 while (f != null);
319 return rc;
323 * Return a git root for the file path (the parent directory with ".git" subdirectory)
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
330 public static VirtualFile getGitRoot(final FilePath filePath) throws VcsException {
331 VirtualFile root = getGitRootOrNull(filePath);
332 if (root != null) {
333 return root;
335 throw new VcsException("The file " + filePath + " is not under git.");
339 * Return a git root for the file path (the parent directory with ".git" subdirectory)
341 * @param filePath a file path
342 * @return git root for the file or null if the file is not under git
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();
350 if (file == null) {
351 return null;
353 return LocalFileSystem.getInstance().findFileByIoFile(file);
357 * Return a git root for the file (the parent directory with ".git" subdirectory)
359 * @param file the file to check
360 * @return git root for the file
361 * @throws VcsException if the file is not under git
363 public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
364 final VirtualFile root = gitRootOrNull(file);
365 if (root != null) {
366 return root;
368 else {
369 throw new VcsException("The file " + file.getPath() + " is not under git.");
374 * Return a git root for the file (the parent directory with ".git" subdirectory)
376 * @param file the file to check
377 * @return git root for the file or null if the file is not not under Git
379 @Nullable
380 public static VirtualFile gitRootOrNull(final VirtualFile file) {
381 if (file instanceof AbstractVcsVirtualFile) {
382 return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
384 VirtualFile root = file;
385 while (root != null) {
386 if (root.findFileByRelativePath(".git") != null) {
387 return root;
389 root = root.getParent();
391 return root;
396 * Check if the virtual file under git
398 * @param vFile a virtual file
399 * @return true if the file is under git
401 public static boolean isUnderGit(final VirtualFile vFile) {
402 return gitRootOrNull(vFile) != null;
406 * Get relative path
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.
413 public static String relativePath(final VirtualFile root, FilePath path) {
414 return relativePath(VfsUtil.virtualToIoFile(root), path.getIOFile());
419 * Get relative path
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.
426 public static String relativePath(final File root, FilePath path) {
427 return relativePath(root, path.getIOFile());
431 * Get relative path
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.
438 public static String relativePath(final File root, VirtualFile file) {
439 return relativePath(root, VfsUtil.virtualToIoFile(file));
443 * Get relative path
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.
450 public static String relativePath(final VirtualFile root, VirtualFile file) {
451 return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
455 * Get relative path
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.
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);
467 return rc.replace(File.separatorChar, '/');
471 * Refresh files
473 * @param project a project
474 * @param affectedFiles affected files and directories
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;
482 file.refresh(false, true);
483 if (file.isDirectory()) {
484 dirty.dirDirtyRecursively(file);
486 else {
487 dirty.fileDirty(file);
493 * Refresh files
495 * @param project a project
496 * @param affectedFiles affected files and directories
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;
504 if (file.isDirectory()) {
505 dirty.dirDirtyRecursively(file);
507 else {
508 dirty.fileDirty(file);
515 * Mark files dirty
517 * @param project a project
518 * @param affectedFiles affected files and directories
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);
526 else {
527 dirty.fileDirty(file);
533 * Refresh files
535 * @param project a project
536 * @param affectedFiles affected files and directories
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);
545 if (file.isDirectory()) {
546 dirty.dirDirtyRecursively(file);
548 else {
549 dirty.fileDirty(file);
555 * Return committer name based on author name and committer name
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
561 public static String adjustAuthorName(final String authorName, String committerName) {
562 if (!authorName.equals(committerName)) {
563 //noinspection HardCodedStringLiteral
564 committerName = authorName + ", via " + committerName;
566 return committerName;
570 * Check if the file path is under git
572 * @param path the path
573 * @return true if the file path is under git
575 public static boolean isUnderGit(final FilePath path) {
576 return getGitRootOrNull(path) != null;
580 * Get git roots for the selected paths
582 * @param filePaths the context paths
583 * @return a set of git roots
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);
593 return rc;
597 * Get git time (UNIX time) basing on the date object
599 * @param time the time to convert
600 * @return the time in git format
602 public static String gitTime(Date time) {
603 long t = time.getTime() / 1000;
604 return Long.toString(t);
608 * Format revision number from long to 16-digit abbreviated revision
610 * @param rev the abbreviated revision number as long
611 * @return the revision string
613 public static String formatLongRev(long rev) {
614 return String.format("%015x%x", (rev >>> 4), rev & 0xF);
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.
620 * @param file the file to get base for
621 * @param path the path to to check
622 * @return the file base
624 @Nullable
625 public static VirtualFile getPossibleBase(VirtualFile file, String... path) {
626 return getPossibleBase(file, path.length, path);
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.
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
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;
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;
649 if (i == n && c != null) {
650 // all components matched
651 return c.getParent();
653 // try shorter paths paths
654 return getPossibleBase(file, n - 1, path);
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>();
664 GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
665 h.setNoSSH(true);
666 h.addParameters("--pretty=format:%x0C%n" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
667 parametersSpecifier.consume(h);
669 String output = h.run();
670 LOG.debug("getLocalCommittedChanges output: '" + output + "'");
671 StringScanner s = new StringScanner(output);
672 while (s.hasMoreData() && s.startsWith('\u000C')) {
673 s.nextLine();
674 consumer.consume(GitChangeUtils.parseChangeList(project, root, s));
676 if (s.hasMoreData()) {
677 throw new IllegalStateException("More input is avaialble: " + s.line());
681 public static List<CommittedChangeList> getLocalCommittedChanges(final Project project,
682 final VirtualFile root,
683 final Consumer<GitSimpleHandler> parametersSpecifier)
684 throws VcsException {
685 final List<CommittedChangeList> rc = new ArrayList<CommittedChangeList>();
687 getLocalCommittedChanges(project, root, parametersSpecifier, new Consumer<CommittedChangeList>() {
688 public void consume(CommittedChangeList committedChangeList) {
689 rc.add(committedChangeList);
693 return rc;
697 * Cast or wrap exception into a vcs exception, errors and runtime exceptions are just thrown throw.
699 * @param t an exception to throw
700 * @return a wrapped exception
702 public static VcsException rethrowVcsException(Throwable t) {
703 if (t instanceof Error) {
704 throw (Error)t;
706 if (t instanceof RuntimeException) {
707 throw (RuntimeException)t;
709 if (t instanceof VcsException) {
710 return (VcsException)t;
712 return new VcsException(t.getMessage(), t);