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
;
44 import java
.io
.UnsupportedEncodingException
;
48 * Git utility/helper methods
50 public class GitUtil
{
53 * A private constructor to suppress instance creation
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
67 public static Map
<VirtualFile
, List
<VirtualFile
>> sortFilesByGitRoot(@NotNull Collection
<VirtualFile
> virtualFiles
) throws VcsException
{
68 return sortFilesByGitRoot(virtualFiles
, false);
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
)
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) {
89 throw new VcsException("The file " + file
.getPath() + " is not under Git");
92 List
<VirtualFile
> files
= result
.get(vcsRoot
);
94 files
= new ArrayList
<VirtualFile
>();
95 result
.put(vcsRoot
, files
);
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
)) {
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
);
151 throw new VcsException("The file " + p
.getPath() + " is not under Git");
154 List
<FilePath
> l
= rc
.get(root
);
156 l
= new ArrayList
<FilePath
>();
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
);
177 //noinspection AssignmentToForLoopParameter
180 throw new VcsException("Unterminated escape sequence in the path: " + path
);
182 final char e
= path
.charAt(i
);
195 // collect sequence of characters as a byte array.
198 for (int j
= i
; j
< l
;) {
199 if (isOctal(path
.charAt(j
))) {
201 for (int k
= 0; k
< 3 && j
< l
&& isOctal(path
.charAt(j
)); k
++) {
202 //noinspection AssignmentToForLoopParameter
206 if (j
+ 1 >= l
|| path
.charAt(j
) != '\\' || !isOctal(path
.charAt(j
+ 1))) {
209 //noinspection AssignmentToForLoopParameter
212 // convert to byte array
213 byte[] b
= new byte[n
];
216 if (isOctal(path
.charAt(i
))) {
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
225 if (i
+ 1 >= l
|| path
.charAt(i
) != '\\' || !isOctal(path
.charAt(i
+ 1))) {
228 //noinspection AssignmentToForLoopParameter
231 assert n
== b
.length
;
232 // add them to string
233 final String encoding
= GitConfigUtil
.getFileNameEncoding();
235 rc
.append(new String(b
, encoding
));
237 catch (UnsupportedEncodingException e1
) {
238 throw new IllegalStateException("The file name encoding is unsuported: " + encoding
);
242 throw new VcsException("Unknown escape sequence '\\" + path
.charAt(i
) + "' in the path: " + path
);
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
;
284 if (f
.findFileByRelativePath(".git") != null) {
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
);
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
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();
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
);
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
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) {
362 root
= root
.getParent();
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;
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());
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());
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
));
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
));
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
);
438 throw new IllegalArgumentException("The file " + path
+ " cannot be made relative to " + root
);
440 return rc
.replace(File
.separatorChar
, '/');
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()) {
455 file
.refresh(false, true);
456 if (file
.isDirectory()) {
457 dirty
.dirDirtyRecursively(file
);
460 dirty
.fileDirty(file
);
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()) {
477 if (file
.isDirectory()) {
478 dirty
.dirDirtyRecursively(file
);
481 dirty
.fileDirty(file
);
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
);
500 dirty
.fileDirty(file
);
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());
516 vFile
.refresh(false, true);
518 if (file
.isDirectory()) {
519 dirty
.dirDirtyRecursively(file
);
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
);
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
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
611 private static VirtualFile
getPossibleBase(VirtualFile file
, int n
, String
... path
) {
612 if (file
== null || n
<= 0 || n
> path
.length
) {
616 VirtualFile c
= file
;
617 for (; c
!= null && i
< n
; i
++, c
= c
.getParent()) {
618 if (!path
[n
- i
].equals(c
.getName())) {
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
);
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')) {
643 rc
.add(GitChangeUtils
.parseChangeList(project
, root
, s
));
645 if (s
.hasMoreData()) {
646 throw new IllegalStateException("More input is avaialble: " + s
.line());