VCS: outgoing provider added, implemented for Git
[fedora-idea.git] / plugins / git4idea / src / git4idea / GitUtil.java
blobf2f2e405ac1e6aa3a28cec06c1075c3d93800667
1 package git4idea;
2 /*
3 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
4 * with the License. You may obtain a copy of the License at:
6 * http://www.apache.org/licenses/LICENSE-2.0
8 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
9 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
10 * the specific language governing permissions and limitations under the License.
12 * Copyright 2007 Decentrix Inc
13 * Copyright 2007 Aspiro AS
14 * Copyright 2008 MQSoftware
15 * Copyright 2008 JetBrains s.r.o.
17 * Authors: gevession, Erlend Simonsen & Mark Scott
19 * This code was originally derived from the MKS & Mercurial IDEA VCS plugins
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.SystemInfo;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.vcs.FilePath;
26 import com.intellij.openapi.vcs.VcsException;
27 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
28 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
29 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
30 import com.intellij.openapi.vfs.LocalFileSystem;
31 import com.intellij.openapi.vfs.VfsUtil;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.util.Consumer;
34 import com.intellij.vcsUtil.VcsUtil;
35 import git4idea.changes.GitChangeUtils;
36 import git4idea.commands.GitHandler;
37 import git4idea.commands.GitSimpleHandler;
38 import git4idea.commands.StringScanner;
39 import git4idea.config.GitConfigUtil;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
43 import java.io.File;
44 import java.io.UnsupportedEncodingException;
45 import java.util.*;
47 /**
48 * Git utility/helper methods
50 public class GitUtil {
52 /**
53 * A private constructor to suppress instance creation
55 private GitUtil() {
56 // do nothing
59 /**
60 * Sort files by Git root
62 * @param virtualFiles files to sort
63 * @return sorted files
64 * @throws VcsException if non git files are passed
66 @NotNull
67 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
68 return sortFilesByGitRoot(virtualFiles, false);
71 /**
72 * Sort files by Git root
74 * @param virtualFiles files to sort
75 * @param ignoreNonGit if true, non-git files are ignored
76 * @return sorted files
77 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
79 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
80 throws VcsException {
81 Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
82 for (VirtualFile file : virtualFiles) {
83 final VirtualFile vcsRoot = gitRootOrNull(file);
84 if (vcsRoot == null) {
85 if (ignoreNonGit) {
86 continue;
88 else {
89 throw new VcsException("The file " + file.getPath() + " is not under Git");
92 List<VirtualFile> files = result.get(vcsRoot);
93 if (files == null) {
94 files = new ArrayList<VirtualFile>();
95 result.put(vcsRoot, files);
97 files.add(file);
99 return result;
102 public static String getRelativeFilePath(VirtualFile file, @NotNull final VirtualFile baseDir) {
103 return getRelativeFilePath(file.getPath(), baseDir);
106 public static String getRelativeFilePath(FilePath file, @NotNull final VirtualFile baseDir) {
107 return getRelativeFilePath(file.getPath(), baseDir);
110 public static String getRelativeFilePath(String file, @NotNull final VirtualFile baseDir) {
111 if (SystemInfo.isWindows) {
112 file = file.replace('\\', '/');
114 final String basePath = baseDir.getPath();
115 if (!file.startsWith(basePath)) {
116 return file;
118 else if (file.equals(basePath)) return ".";
119 return file.substring(baseDir.getPath().length() + 1);
123 * Sort files by vcs root
125 * @param files files to sort.
126 * @return the map from root to the files under the root
127 * @throws VcsException if non git files are passed
129 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
130 return sortFilePathsByGitRoot(files, false);
134 * Sort files by vcs root
136 * @param files files to sort.
137 * @param ignoreNonGit if true, non-git files are ignored
138 * @return the map from root to the files under the root
139 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
141 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(Collection<FilePath> files, boolean ignoreNonGit)
142 throws VcsException {
143 Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
144 for (FilePath p : files) {
145 VirtualFile root = getGitRootOrNull(p);
146 if (root == null) {
147 if (ignoreNonGit) {
148 continue;
150 else {
151 throw new VcsException("The file " + p.getPath() + " is not under Git");
154 List<FilePath> l = rc.get(root);
155 if (l == null) {
156 l = new ArrayList<FilePath>();
157 rc.put(root, l);
159 l.add(p);
161 return rc;
165 * Unescape path returned by the Git
167 * @param path a path to unescape
168 * @return unescaped path
169 * @throws VcsException if the path in invalid
171 public static String unescapePath(String path) throws VcsException {
172 final int l = path.length();
173 StringBuilder rc = new StringBuilder(l);
174 for (int i = 0; i < path.length(); i++) {
175 char c = path.charAt(i);
176 if (c == '\\') {
177 //noinspection AssignmentToForLoopParameter
178 i++;
179 if (i >= l) {
180 throw new VcsException("Unterminated escape sequence in the path: " + path);
182 final char e = path.charAt(i);
183 switch (e) {
184 case '\\':
185 rc.append('\\');
186 break;
187 case 't':
188 rc.append('\t');
189 break;
190 case 'n':
191 rc.append('\n');
192 break;
193 default:
194 if (isOctal(e)) {
195 // collect sequence of characters as a byte array.
196 // count bytes first
197 int n = 0;
198 for (int j = i; j < l;) {
199 if (isOctal(path.charAt(j))) {
200 n++;
201 for (int k = 0; k < 3 && j < l && isOctal(path.charAt(j)); k++) {
202 //noinspection AssignmentToForLoopParameter
203 j++;
206 if (j + 1 >= l || path.charAt(j) != '\\' || !isOctal(path.charAt(j + 1))) {
207 break;
209 //noinspection AssignmentToForLoopParameter
210 j++;
212 // convert to byte array
213 byte[] b = new byte[n];
214 n = 0;
215 while (i < l) {
216 if (isOctal(path.charAt(i))) {
217 int code = 0;
218 for (int k = 0; k < 3 && i < l && isOctal(path.charAt(i)); k++) {
219 code = code * 8 + (path.charAt(i) - '0');
220 //noinspection AssignmentToForLoopParameter
221 i++;
223 b[n++] = (byte)code;
225 if (i + 1 >= l || path.charAt(i) != '\\' || !isOctal(path.charAt(i + 1))) {
226 break;
228 //noinspection AssignmentToForLoopParameter
229 i++;
231 assert n == b.length;
232 // add them to string
233 final String encoding = GitConfigUtil.getFileNameEncoding();
234 try {
235 rc.append(new String(b, encoding));
237 catch (UnsupportedEncodingException e1) {
238 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
241 else {
242 throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
246 else {
247 rc.append(c);
250 return rc.toString();
254 * Check if character is octal digit
256 * @param ch a character to test
257 * @return true if the octal digit, false otherwise
259 private static boolean isOctal(char ch) {
260 return '0' <= ch && ch <= '7';
264 * Parse UNIX timestamp as it is returned by the git
266 * @param value a value to parse
267 * @return timestamp as {@link Date} object
269 public static Date parseTimestamp(String value) {
270 return new Date(Long.parseLong(value.trim()) * 1000);
274 * Get git roots from content roots
276 * @param roots git content roots
277 * @return a content root
279 public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
280 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
281 for (VirtualFile root : roots) {
282 VirtualFile f = root;
283 do {
284 if (f.findFileByRelativePath(".git") != null) {
285 rc.add(f);
286 break;
288 f = f.getParent();
290 while (f != null);
292 return rc;
296 * Return a git root for the file path (the parent directory with ".git" subdirectory)
298 * @param filePath a file path
299 * @return git root for the file
300 * @throws IllegalArgumentException if the file is not under git
301 * @throws VcsException if the file is not under git
303 public static VirtualFile getGitRoot(final FilePath filePath) throws VcsException {
304 VirtualFile root = getGitRootOrNull(filePath);
305 if (root != null) {
306 return root;
308 throw new VcsException("The file " + filePath + " is not under git.");
312 * Return a git root for the file path (the parent directory with ".git" subdirectory)
314 * @param filePath a file path
315 * @return git root for the file or null if the file is not under git
317 @Nullable
318 public static VirtualFile getGitRootOrNull(final FilePath filePath) {
319 File file = filePath.getIOFile();
320 while (file != null && (!file.exists() || !file.isDirectory() || !new File(file, ".git").exists())) {
321 file = file.getParentFile();
323 if (file == null) {
324 return null;
326 return LocalFileSystem.getInstance().findFileByIoFile(file);
330 * Return a git root for the file (the parent directory with ".git" subdirectory)
332 * @param file the file to check
333 * @return git root for the file
334 * @throws VcsException if the file is not under git
336 public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
337 final VirtualFile root = gitRootOrNull(file);
338 if (root != null) {
339 return root;
341 else {
342 throw new VcsException("The file " + file.getPath() + " is not under git.");
347 * Return a git root for the file (the parent directory with ".git" subdirectory)
349 * @param file the file to check
350 * @return git root for the file or null if the file is not not under Git
352 @Nullable
353 public static VirtualFile gitRootOrNull(final VirtualFile file) {
354 if (file instanceof AbstractVcsVirtualFile) {
355 return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
357 VirtualFile root = file;
358 while (root != null) {
359 if (root.findFileByRelativePath(".git") != null) {
360 return root;
362 root = root.getParent();
364 return root;
369 * Check if the virtual file under git
371 * @param vFile a virtual file
372 * @return true if the file is under git
374 public static boolean isUnderGit(final VirtualFile vFile) {
375 return gitRootOrNull(vFile) != null;
379 * Get relative path
381 * @param root a root path
382 * @param path a path to file (possibly deleted file)
383 * @return a relative path
384 * @throws IllegalArgumentException if path is not under root.
386 public static String relativePath(final VirtualFile root, FilePath path) {
387 return relativePath(VfsUtil.virtualToIoFile(root), path.getIOFile());
392 * Get relative path
394 * @param root a root path
395 * @param path a path to file (possibly deleted file)
396 * @return a relative path
397 * @throws IllegalArgumentException if path is not under root.
399 public static String relativePath(final File root, FilePath path) {
400 return relativePath(root, path.getIOFile());
404 * Get relative path
406 * @param root a root path
407 * @param file a virtual file
408 * @return a relative path
409 * @throws IllegalArgumentException if path is not under root.
411 public static String relativePath(final File root, VirtualFile file) {
412 return relativePath(root, VfsUtil.virtualToIoFile(file));
416 * Get relative path
418 * @param root a root file
419 * @param file a virtual file
420 * @return a relative path
421 * @throws IllegalArgumentException if path is not under root.
423 public static String relativePath(final VirtualFile root, VirtualFile file) {
424 return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
428 * Get relative path
430 * @param root a root path
431 * @param path a path to file (possibly deleted file)
432 * @return a relative path
433 * @throws IllegalArgumentException if path is not under root.
435 public static String relativePath(final File root, File path) {
436 String rc = FileUtil.getRelativePath(root, path);
437 if (rc == null) {
438 throw new IllegalArgumentException("The file " + path + " cannot be made relative to " + root);
440 return rc.replace(File.separatorChar, '/');
444 * Refresh files
446 * @param project a project
447 * @param affectedFiles affected files and directories
449 public static void refreshFiles(@NotNull final Project project, @NotNull final Collection<VirtualFile> affectedFiles) {
450 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
451 for (VirtualFile file : affectedFiles) {
452 if (!file.isValid()) {
453 continue;
455 file.refresh(false, true);
456 if (file.isDirectory()) {
457 dirty.dirDirtyRecursively(file);
459 else {
460 dirty.fileDirty(file);
466 * Refresh files
468 * @param project a project
469 * @param affectedFiles affected files and directories
471 public static void markFilesDirty(@NotNull final Project project, @NotNull final Collection<VirtualFile> affectedFiles) {
472 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
473 for (VirtualFile file : affectedFiles) {
474 if (!file.isValid()) {
475 continue;
477 if (file.isDirectory()) {
478 dirty.dirDirtyRecursively(file);
480 else {
481 dirty.fileDirty(file);
488 * Mark files dirty
490 * @param project a project
491 * @param affectedFiles affected files and directories
493 public static void markFilesDirty(Project project, List<FilePath> affectedFiles) {
494 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
495 for (FilePath file : affectedFiles) {
496 if (file.isDirectory()) {
497 dirty.dirDirtyRecursively(file);
499 else {
500 dirty.fileDirty(file);
506 * Refresh files
508 * @param project a project
509 * @param affectedFiles affected files and directories
511 public static void refreshFiles(Project project, List<FilePath> affectedFiles) {
512 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
513 for (FilePath file : affectedFiles) {
514 VirtualFile vFile = VcsUtil.getVirtualFile(file.getIOFile());
515 if (vFile != null) {
516 vFile.refresh(false, true);
518 if (file.isDirectory()) {
519 dirty.dirDirtyRecursively(file);
521 else {
522 dirty.fileDirty(file);
528 * Return committer name based on author name and committer name
530 * @param authorName the name of author
531 * @param committerName the name of committer
532 * @return just a name if they are equal, or name that includes both author and committer
534 public static String adjustAuthorName(final String authorName, String committerName) {
535 if (!authorName.equals(committerName)) {
536 //noinspection HardCodedStringLiteral
537 committerName = authorName + ", via " + committerName;
539 return committerName;
543 * Check if the file path is under git
545 * @param path the path
546 * @return true if the file path is under git
548 public static boolean isUnderGit(final FilePath path) {
549 return getGitRootOrNull(path) != null;
553 * Get git roots for the selected paths
555 * @param filePaths the context paths
556 * @return a set of git roots
558 public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
559 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
560 for (FilePath path : filePaths) {
561 final VirtualFile root = getGitRootOrNull(path);
562 if (root != null) {
563 rc.add(root);
566 return rc;
570 * Get git time (UNIX time) basing on the date object
572 * @param time the time to convert
573 * @return the time in git format
575 public static String gitTime(Date time) {
576 long t = time.getTime() / 1000;
577 return Long.toString(t);
581 * Format revision number from long to 16-digit abbreviated revision
583 * @param rev the abbreviated revision number as long
584 * @return the revision string
586 public static String formatLongRev(long rev) {
587 return String.format("%015x%x", (rev >>> 4), rev & 0xF);
591 * 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.
593 * @param file the file to get base for
594 * @param path the path to to check
595 * @return the file base
597 @Nullable
598 public static VirtualFile getPossibleBase(VirtualFile file, String... path) {
599 return getPossibleBase(file, path.length, path);
603 * 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.
605 * @param file the file to get base for
606 * @param n the length of the path to check
607 * @param path the path to to check
608 * @return the file base
610 @Nullable
611 private static VirtualFile getPossibleBase(VirtualFile file, int n, String... path) {
612 if (file == null || n <= 0 || n > path.length) {
613 return null;
615 int i = 1;
616 VirtualFile c = file;
617 for (; c != null && i < n; i++, c = c.getParent()) {
618 if (!path[n - i].equals(c.getName())) {
619 break;
622 if (i == n && c != null) {
623 // all components matched
624 return c.getParent();
626 // try shorter paths paths
627 return getPossibleBase(file, n - 1, path);
630 public static List<CommittedChangeList> getLocalCommittedChanges(final Project project, final VirtualFile root,
631 final Consumer<GitSimpleHandler> parametersSpecifier) throws VcsException {
632 final List<CommittedChangeList> rc = new ArrayList<CommittedChangeList>();
634 GitSimpleHandler h = new GitSimpleHandler(project, root, GitHandler.LOG);
635 h.setNoSSH(true);
636 h.addParameters("--pretty=format:%x0C%n" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT);
637 parametersSpecifier.consume(h);
639 String output = h.run();
640 StringScanner s = new StringScanner(output);
641 while (s.hasMoreData() && s.startsWith('\u000C')) {
642 s.nextLine();
643 rc.add(GitChangeUtils.parseChangeList(project, root, s));
645 if (s.hasMoreData()) {
646 throw new IllegalStateException("More input is avaialble: " + s.line());
648 return rc;