git4idea: Implemeted forced merge/rebase and stash options for update operation
[fedora-idea.git] / plugins / git4idea / src / git4idea / GitUtil.java
blob5dd6488f1e3c5bcb0b0d42b9ec36db0c0e499d7d
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.diagnostic.Logger;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.io.FileUtil;
26 import com.intellij.openapi.vcs.FilePath;
27 import com.intellij.openapi.vcs.VcsException;
28 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
29 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
30 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
31 import com.intellij.openapi.vfs.LocalFileSystem;
32 import com.intellij.openapi.vfs.VfsUtil;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.util.Consumer;
35 import com.intellij.vcsUtil.VcsUtil;
36 import git4idea.changes.GitChangeUtils;
37 import git4idea.commands.GitHandler;
38 import git4idea.commands.GitSimpleHandler;
39 import git4idea.commands.StringScanner;
40 import git4idea.config.GitConfigUtil;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
44 import java.io.File;
45 import java.io.UnsupportedEncodingException;
46 import java.util.*;
48 /**
49 * Git utility/helper methods
51 public class GitUtil {
52 /**
53 * The logger instance
55 private final static Logger LOG = Logger.getInstance("#git4idea.GitUtil");
56 /**
57 * Comparator for virtual files by name
59 public static final Comparator<VirtualFile> VIRTUAL_FILE_COMPARATOR = new Comparator<VirtualFile>() {
60 public int compare(final VirtualFile o1, final VirtualFile o2) {
61 if (o1 == null && o2 == null) {
62 return 0;
64 if (o1 == null) {
65 return -1;
67 if (o2 == null) {
68 return 1;
70 return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
74 /**
75 * A private constructor to suppress instance creation
77 private GitUtil() {
78 // do nothing
81 /**
82 * Sort files by Git root
84 * @param virtualFiles files to sort
85 * @return sorted files
86 * @throws VcsException if non git files are passed
88 @NotNull
89 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
90 return sortFilesByGitRoot(virtualFiles, false);
93 /**
94 * Sort files by Git root
96 * @param virtualFiles files to sort
97 * @param ignoreNonGit if true, non-git files are ignored
98 * @return sorted files
99 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
101 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
102 throws VcsException {
103 Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
104 for (VirtualFile file : virtualFiles) {
105 final VirtualFile vcsRoot = gitRootOrNull(file);
106 if (vcsRoot == null) {
107 if (ignoreNonGit) {
108 continue;
110 else {
111 throw new VcsException("The file " + file.getPath() + " is not under Git");
114 List<VirtualFile> files = result.get(vcsRoot);
115 if (files == null) {
116 files = new ArrayList<VirtualFile>();
117 result.put(vcsRoot, files);
119 files.add(file);
121 return result;
124 public static String getRelativeFilePath(VirtualFile file, @NotNull final VirtualFile baseDir) {
125 return getRelativeFilePath(file.getPath(), baseDir);
128 public static String getRelativeFilePath(FilePath file, @NotNull final VirtualFile baseDir) {
129 return getRelativeFilePath(file.getPath(), baseDir);
132 public static String getRelativeFilePath(String file, @NotNull final VirtualFile baseDir) {
133 if (SystemInfo.isWindows) {
134 file = file.replace('\\', '/');
136 final String basePath = baseDir.getPath();
137 if (!file.startsWith(basePath)) {
138 return file;
140 else if (file.equals(basePath)) return ".";
141 return file.substring(baseDir.getPath().length() + 1);
145 * Sort files by vcs root
147 * @param files files to sort.
148 * @return the map from root to the files under the root
149 * @throws VcsException if non git files are passed
151 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
152 return sortFilePathsByGitRoot(files, false);
156 * Sort files by vcs root
158 * @param files files to sort.
159 * @param ignoreNonGit if true, non-git files are ignored
160 * @return the map from root to the files under the root
161 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
163 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(Collection<FilePath> files, boolean ignoreNonGit)
164 throws VcsException {
165 Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
166 for (FilePath p : files) {
167 VirtualFile root = getGitRootOrNull(p);
168 if (root == null) {
169 if (ignoreNonGit) {
170 continue;
172 else {
173 throw new VcsException("The file " + p.getPath() + " is not under Git");
176 List<FilePath> l = rc.get(root);
177 if (l == null) {
178 l = new ArrayList<FilePath>();
179 rc.put(root, l);
181 l.add(p);
183 return rc;
187 * Unescape path returned by the Git
189 * @param path a path to unescape
190 * @return unescaped path
191 * @throws VcsException if the path in invalid
193 public static String unescapePath(String path) throws VcsException {
194 final int l = path.length();
195 StringBuilder rc = new StringBuilder(l);
196 for (int i = 0; i < path.length(); i++) {
197 char c = path.charAt(i);
198 if (c == '\\') {
199 //noinspection AssignmentToForLoopParameter
200 i++;
201 if (i >= l) {
202 throw new VcsException("Unterminated escape sequence in the path: " + path);
204 final char e = path.charAt(i);
205 switch (e) {
206 case '\\':
207 rc.append('\\');
208 break;
209 case 't':
210 rc.append('\t');
211 break;
212 case 'n':
213 rc.append('\n');
214 break;
215 default:
216 if (isOctal(e)) {
217 // collect sequence of characters as a byte array.
218 // count bytes first
219 int n = 0;
220 for (int j = i; j < l;) {
221 if (isOctal(path.charAt(j))) {
222 n++;
223 for (int k = 0; k < 3 && j < l && isOctal(path.charAt(j)); k++) {
224 //noinspection AssignmentToForLoopParameter
225 j++;
228 if (j + 1 >= l || path.charAt(j) != '\\' || !isOctal(path.charAt(j + 1))) {
229 break;
231 //noinspection AssignmentToForLoopParameter
232 j++;
234 // convert to byte array
235 byte[] b = new byte[n];
236 n = 0;
237 while (i < l) {
238 if (isOctal(path.charAt(i))) {
239 int code = 0;
240 for (int k = 0; k < 3 && i < l && isOctal(path.charAt(i)); k++) {
241 code = code * 8 + (path.charAt(i) - '0');
242 //noinspection AssignmentToForLoopParameter
243 i++;
245 b[n++] = (byte)code;
247 if (i + 1 >= l || path.charAt(i) != '\\' || !isOctal(path.charAt(i + 1))) {
248 break;
250 //noinspection AssignmentToForLoopParameter
251 i++;
253 assert n == b.length;
254 // add them to string
255 final String encoding = GitConfigUtil.getFileNameEncoding();
256 try {
257 rc.append(new String(b, encoding));
259 catch (UnsupportedEncodingException e1) {
260 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
263 else {
264 throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
268 else {
269 rc.append(c);
272 return rc.toString();
276 * Check if character is octal digit
278 * @param ch a character to test
279 * @return true if the octal digit, false otherwise
281 private static boolean isOctal(char ch) {
282 return '0' <= ch && ch <= '7';
286 * Parse UNIX timestamp as it is returned by the git
288 * @param value a value to parse
289 * @return timestamp as {@link Date} object
291 public static Date parseTimestamp(String value) {
292 return new Date(Long.parseLong(value.trim()) * 1000);
296 * Get git roots from content roots
298 * @param roots git content roots
299 * @return a content root
301 public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
302 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
303 for (VirtualFile root : roots) {
304 VirtualFile f = root;
305 do {
306 if (f.findFileByRelativePath(".git") != null) {
307 rc.add(f);
308 break;
310 f = f.getParent();
312 while (f != null);
314 return rc;
318 * Return a git root for the file path (the parent directory with ".git" subdirectory)
320 * @param filePath a file path
321 * @return git root for the file
322 * @throws IllegalArgumentException if the file is not under git
323 * @throws VcsException if the file is not under git
325 public static VirtualFile getGitRoot(final FilePath filePath) throws VcsException {
326 VirtualFile root = getGitRootOrNull(filePath);
327 if (root != null) {
328 return root;
330 throw new VcsException("The file " + filePath + " is not under git.");
334 * Return a git root for the file path (the parent directory with ".git" subdirectory)
336 * @param filePath a file path
337 * @return git root for the file or null if the file is not under git
339 @Nullable
340 public static VirtualFile getGitRootOrNull(final FilePath filePath) {
341 File file = filePath.getIOFile();
342 while (file != null && (!file.exists() || !file.isDirectory() || !new File(file, ".git").exists())) {
343 file = file.getParentFile();
345 if (file == null) {
346 return null;
348 return LocalFileSystem.getInstance().findFileByIoFile(file);
352 * Return a git root for the file (the parent directory with ".git" subdirectory)
354 * @param file the file to check
355 * @return git root for the file
356 * @throws VcsException if the file is not under git
358 public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
359 final VirtualFile root = gitRootOrNull(file);
360 if (root != null) {
361 return root;
363 else {
364 throw new VcsException("The file " + file.getPath() + " is not under git.");
369 * Return a git root for the file (the parent directory with ".git" subdirectory)
371 * @param file the file to check
372 * @return git root for the file or null if the file is not not under Git
374 @Nullable
375 public static VirtualFile gitRootOrNull(final VirtualFile file) {
376 if (file instanceof AbstractVcsVirtualFile) {
377 return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
379 VirtualFile root = file;
380 while (root != null) {
381 if (root.findFileByRelativePath(".git") != null) {
382 return root;
384 root = root.getParent();
386 return root;
391 * Check if the virtual file under git
393 * @param vFile a virtual file
394 * @return true if the file is under git
396 public static boolean isUnderGit(final VirtualFile vFile) {
397 return gitRootOrNull(vFile) != null;
401 * Get relative path
403 * @param root a root path
404 * @param path a path to file (possibly deleted file)
405 * @return a relative path
406 * @throws IllegalArgumentException if path is not under root.
408 public static String relativePath(final VirtualFile root, FilePath path) {
409 return relativePath(VfsUtil.virtualToIoFile(root), path.getIOFile());
414 * Get relative path
416 * @param root a root path
417 * @param path a path to file (possibly deleted file)
418 * @return a relative path
419 * @throws IllegalArgumentException if path is not under root.
421 public static String relativePath(final File root, FilePath path) {
422 return relativePath(root, path.getIOFile());
426 * Get relative path
428 * @param root a root path
429 * @param file a virtual file
430 * @return a relative path
431 * @throws IllegalArgumentException if path is not under root.
433 public static String relativePath(final File root, VirtualFile file) {
434 return relativePath(root, VfsUtil.virtualToIoFile(file));
438 * Get relative path
440 * @param root a root file
441 * @param file a virtual file
442 * @return a relative path
443 * @throws IllegalArgumentException if path is not under root.
445 public static String relativePath(final VirtualFile root, VirtualFile file) {
446 return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
450 * Get relative path
452 * @param root a root path
453 * @param path a path to file (possibly deleted file)
454 * @return a relative path
455 * @throws IllegalArgumentException if path is not under root.
457 public static String relativePath(final File root, File path) {
458 String rc = FileUtil.getRelativePath(root, path);
459 if (rc == null) {
460 throw new IllegalArgumentException("The file " + path + " cannot be made relative to " + root);
462 return rc.replace(File.separatorChar, '/');
466 * Refresh files
468 * @param project a project
469 * @param affectedFiles affected files and directories
471 public static void refreshFiles(@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 file.refresh(false, true);
478 if (file.isDirectory()) {
479 dirty.dirDirtyRecursively(file);
481 else {
482 dirty.fileDirty(file);
488 * Refresh files
490 * @param project a project
491 * @param affectedFiles affected files and directories
493 public static void markFilesDirty(@NotNull final Project project, @NotNull final Collection<VirtualFile> affectedFiles) {
494 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
495 for (VirtualFile file : affectedFiles) {
496 if (!file.isValid()) {
497 continue;
499 if (file.isDirectory()) {
500 dirty.dirDirtyRecursively(file);
502 else {
503 dirty.fileDirty(file);
510 * Mark files dirty
512 * @param project a project
513 * @param affectedFiles affected files and directories
515 public static void markFilesDirty(Project project, List<FilePath> affectedFiles) {
516 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
517 for (FilePath file : affectedFiles) {
518 if (file.isDirectory()) {
519 dirty.dirDirtyRecursively(file);
521 else {
522 dirty.fileDirty(file);
528 * Refresh files
530 * @param project a project
531 * @param affectedFiles affected files and directories
533 public static void refreshFiles(Project project, List<FilePath> affectedFiles) {
534 final VcsDirtyScopeManager dirty = VcsDirtyScopeManager.getInstance(project);
535 for (FilePath file : affectedFiles) {
536 VirtualFile vFile = VcsUtil.getVirtualFile(file.getIOFile());
537 if (vFile != null) {
538 vFile.refresh(false, true);
540 if (file.isDirectory()) {
541 dirty.dirDirtyRecursively(file);
543 else {
544 dirty.fileDirty(file);
550 * Return committer name based on author name and committer name
552 * @param authorName the name of author
553 * @param committerName the name of committer
554 * @return just a name if they are equal, or name that includes both author and committer
556 public static String adjustAuthorName(final String authorName, String committerName) {
557 if (!authorName.equals(committerName)) {
558 //noinspection HardCodedStringLiteral
559 committerName = authorName + ", via " + committerName;
561 return committerName;
565 * Check if the file path is under git
567 * @param path the path
568 * @return true if the file path is under git
570 public static boolean isUnderGit(final FilePath path) {
571 return getGitRootOrNull(path) != null;
575 * Get git roots for the selected paths
577 * @param filePaths the context paths
578 * @return a set of git roots
580 public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
581 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
582 for (FilePath path : filePaths) {
583 final VirtualFile root = getGitRootOrNull(path);
584 if (root != null) {
585 rc.add(root);
588 return rc;
592 * Get git time (UNIX time) basing on the date object
594 * @param time the time to convert
595 * @return the time in git format
597 public static String gitTime(Date time) {
598 long t = time.getTime() / 1000;
599 return Long.toString(t);
603 * Format revision number from long to 16-digit abbreviated revision
605 * @param rev the abbreviated revision number as long
606 * @return the revision string
608 public static String formatLongRev(long rev) {
609 return String.format("%015x%x", (rev >>> 4), rev & 0xF);
613 * 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.
615 * @param file the file to get base for
616 * @param path the path to to check
617 * @return the file base
619 @Nullable
620 public static VirtualFile getPossibleBase(VirtualFile file, String... path) {
621 return getPossibleBase(file, path.length, path);
625 * 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.
627 * @param file the file to get base for
628 * @param n the length of the path to check
629 * @param path the path to to check
630 * @return the file base
632 @Nullable
633 private static VirtualFile getPossibleBase(VirtualFile file, int n, String... path) {
634 if (file == null || n <= 0 || n > path.length) {
635 return null;
637 int i = 1;
638 VirtualFile c = file;
639 for (; c != null && i < n; i++, c = c.getParent()) {
640 if (!path[n - i].equals(c.getName())) {
641 break;
644 if (i == n && c != null) {
645 // all components matched
646 return c.getParent();
648 // try shorter paths paths
649 return getPossibleBase(file, n - 1, path);
652 public static List<CommittedChangeList> getLocalCommittedChanges(final Project project,
653 final VirtualFile root,
654 final Consumer<GitSimpleHandler> parametersSpecifier)
655 throws VcsException {
656 final List<CommittedChangeList> rc = new ArrayList<CommittedChangeList>();
658 GitSimpleHandler h = new GitSimpleHandler(project, root, GitHandler.LOG);
659 h.setNoSSH(true);
660 h.addParameters("--pretty=format:%x0C%n" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
661 parametersSpecifier.consume(h);
663 String output = h.run();
664 LOG.debug("getLocalCommittedChanges output: '" + output + "'");
665 StringScanner s = new StringScanner(output);
666 while (s.hasMoreData() && s.startsWith('\u000C')) {
667 s.nextLine();
668 rc.add(GitChangeUtils.parseChangeList(project, root, s));
670 if (s.hasMoreData()) {
671 throw new IllegalStateException("More input is avaialble: " + s.line());
673 return rc;
677 * Cast or wrap exception into a vcs exception, errors and runtime exceptions are just thrown throw.
679 * @param t an exception to throw
680 * @return a wrapped exception
682 public static VcsException rethrowVcsException(Throwable t) {
683 if (t instanceof Error) {
684 throw (Error)t;
686 if (t instanceof RuntimeException) {
687 throw (RuntimeException)t;
689 if (t instanceof VcsException) {
690 return (VcsException)t;
692 return new VcsException(t.getMessage(), t);