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
;
45 import java
.io
.UnsupportedEncodingException
;
49 * Git utility/helper methods
51 public class GitUtil
{
55 private final static Logger LOG
= Logger
.getInstance("#git4idea.GitUtil");
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) {
70 return o1
.getPresentableUrl().compareTo(o2
.getPresentableUrl());
75 * A private constructor to suppress instance creation
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
89 public static Map
<VirtualFile
, List
<VirtualFile
>> sortFilesByGitRoot(@NotNull Collection
<VirtualFile
> virtualFiles
) throws VcsException
{
90 return sortFilesByGitRoot(virtualFiles
, false);
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) {
111 throw new VcsException("The file " + file
.getPath() + " is not under Git");
114 List
<VirtualFile
> files
= result
.get(vcsRoot
);
116 files
= new ArrayList
<VirtualFile
>();
117 result
.put(vcsRoot
, files
);
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
)) {
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
);
173 throw new VcsException("The file " + p
.getPath() + " is not under Git");
176 List
<FilePath
> l
= rc
.get(root
);
178 l
= new ArrayList
<FilePath
>();
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
);
199 //noinspection AssignmentToForLoopParameter
202 throw new VcsException("Unterminated escape sequence in the path: " + path
);
204 final char e
= path
.charAt(i
);
217 // collect sequence of characters as a byte array.
220 for (int j
= i
; j
< l
;) {
221 if (isOctal(path
.charAt(j
))) {
223 for (int k
= 0; k
< 3 && j
< l
&& isOctal(path
.charAt(j
)); k
++) {
224 //noinspection AssignmentToForLoopParameter
228 if (j
+ 1 >= l
|| path
.charAt(j
) != '\\' || !isOctal(path
.charAt(j
+ 1))) {
231 //noinspection AssignmentToForLoopParameter
234 // convert to byte array
235 byte[] b
= new byte[n
];
238 if (isOctal(path
.charAt(i
))) {
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
247 if (i
+ 1 >= l
|| path
.charAt(i
) != '\\' || !isOctal(path
.charAt(i
+ 1))) {
250 //noinspection AssignmentToForLoopParameter
253 assert n
== b
.length
;
254 // add them to string
255 final String encoding
= GitConfigUtil
.getFileNameEncoding();
257 rc
.append(new String(b
, encoding
));
259 catch (UnsupportedEncodingException e1
) {
260 throw new IllegalStateException("The file name encoding is unsuported: " + encoding
);
264 throw new VcsException("Unknown escape sequence '\\" + path
.charAt(i
) + "' in the path: " + path
);
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
;
306 if (f
.findFileByRelativePath(".git") != null) {
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
);
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
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();
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
);
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
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) {
384 root
= root
.getParent();
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;
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());
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());
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
));
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
));
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
);
460 throw new IllegalArgumentException("The file " + path
+ " cannot be made relative to " + root
);
462 return rc
.replace(File
.separatorChar
, '/');
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()) {
477 file
.refresh(false, true);
478 if (file
.isDirectory()) {
479 dirty
.dirDirtyRecursively(file
);
482 dirty
.fileDirty(file
);
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()) {
499 if (file
.isDirectory()) {
500 dirty
.dirDirtyRecursively(file
);
503 dirty
.fileDirty(file
);
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
);
522 dirty
.fileDirty(file
);
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());
538 vFile
.refresh(false, true);
540 if (file
.isDirectory()) {
541 dirty
.dirDirtyRecursively(file
);
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
);
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
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
633 private static VirtualFile
getPossibleBase(VirtualFile file
, int n
, String
... path
) {
634 if (file
== null || n
<= 0 || n
> path
.length
) {
638 VirtualFile c
= file
;
639 for (; c
!= null && i
< n
; i
++, c
= c
.getParent()) {
640 if (!path
[n
- i
].equals(c
.getName())) {
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
);
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')) {
668 rc
.add(GitChangeUtils
.parseChangeList(project
, root
, s
));
670 if (s
.hasMoreData()) {
671 throw new IllegalStateException("More input is avaialble: " + s
.line());
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
) {
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
);