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.
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
;
41 import java
.io
.UnsupportedEncodingException
;
42 import java
.nio
.charset
.Charset
;
46 * Git utility/helper methods
48 public class GitUtil
{
52 private final static Logger LOG
= Logger
.getInstance("#git4idea.GitUtil");
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) {
67 return o1
.getPresentableUrl().compareTo(o2
.getPresentableUrl());
71 * The UTF-8 encoding name
73 public static final String UTF8_ENCODING
= "UTF-8";
77 public static final Charset UTF8_CHARSET
= Charset
.forName(UTF8_ENCODING
);
80 * A private constructor to suppress instance creation
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
94 public static Map
<VirtualFile
, List
<VirtualFile
>> sortFilesByGitRoot(@NotNull Collection
<VirtualFile
> virtualFiles
) throws VcsException
{
95 return sortFilesByGitRoot(virtualFiles
, false);
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) {
116 throw new VcsException("The file " + file
.getPath() + " is not under Git");
119 List
<VirtualFile
> files
= result
.get(vcsRoot
);
121 files
= new ArrayList
<VirtualFile
>();
122 result
.put(vcsRoot
, files
);
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
)) {
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
);
178 throw new VcsException("The file " + p
.getPath() + " is not under Git");
181 List
<FilePath
> l
= rc
.get(root
);
183 l
= new ArrayList
<FilePath
>();
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
);
204 //noinspection AssignmentToForLoopParameter
207 throw new VcsException("Unterminated escape sequence in the path: " + path
);
209 final char e
= path
.charAt(i
);
222 // collect sequence of characters as a byte array.
225 for (int j
= i
; j
< l
;) {
226 if (isOctal(path
.charAt(j
))) {
228 for (int k
= 0; k
< 3 && j
< l
&& isOctal(path
.charAt(j
)); k
++) {
229 //noinspection AssignmentToForLoopParameter
233 if (j
+ 1 >= l
|| path
.charAt(j
) != '\\' || !isOctal(path
.charAt(j
+ 1))) {
236 //noinspection AssignmentToForLoopParameter
239 // convert to byte array
240 byte[] b
= new byte[n
];
243 if (isOctal(path
.charAt(i
))) {
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
252 if (i
+ 1 >= l
|| path
.charAt(i
) != '\\' || !isOctal(path
.charAt(i
+ 1))) {
255 //noinspection AssignmentToForLoopParameter
258 assert n
== b
.length
;
259 // add them to string
260 final String encoding
= GitConfigUtil
.getFileNameEncoding();
262 rc
.append(new String(b
, encoding
));
264 catch (UnsupportedEncodingException e1
) {
265 throw new IllegalStateException("The file name encoding is unsuported: " + encoding
);
269 throw new VcsException("Unknown escape sequence '\\" + path
.charAt(i
) + "' in the path: " + path
);
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
;
311 if (f
.findFileByRelativePath(".git") != null) {
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
);
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
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();
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
);
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
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) {
389 root
= root
.getParent();
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;
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());
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());
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
));
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
));
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
);
465 throw new IllegalArgumentException("The file " + path
+ " cannot be made relative to " + root
);
467 return rc
.replace(File
.separatorChar
, '/');
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()) {
482 file
.refresh(false, true);
483 if (file
.isDirectory()) {
484 dirty
.dirDirtyRecursively(file
);
487 dirty
.fileDirty(file
);
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()) {
504 if (file
.isDirectory()) {
505 dirty
.dirDirtyRecursively(file
);
508 dirty
.fileDirty(file
);
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
);
527 dirty
.fileDirty(file
);
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());
543 vFile
.refresh(false, true);
545 if (file
.isDirectory()) {
546 dirty
.dirDirtyRecursively(file
);
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
);
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
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
638 private static VirtualFile
getPossibleBase(VirtualFile file
, int n
, String
... path
) {
639 if (file
== null || n
<= 0 || n
> path
.length
) {
643 VirtualFile c
= file
;
644 for (; c
!= null && i
< n
; i
++, c
= c
.getParent()) {
645 if (!path
[n
- i
].equals(c
.getName())) {
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
);
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')) {
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
);
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
) {
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
);