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.components.ServiceManager;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.progress.ProgressIndicator;
21 import com.intellij.openapi.progress.Task;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.Pair;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vcs.AbstractVcsHelper;
27 import com.intellij.openapi.vcs.FilePath;
28 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
29 import com.intellij.openapi.vcs.VcsException;
30 import com.intellij.openapi.vcs.changes.Change;
31 import com.intellij.openapi.vcs.changes.ChangeListManager;
32 import com.intellij.openapi.vcs.changes.FilePathsHelper;
33 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
34 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
35 import com.intellij.openapi.vfs.LocalFileSystem;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.util.Consumer;
38 import com.intellij.util.Function;
39 import com.intellij.util.ui.UIUtil;
40 import com.intellij.vcsUtil.VcsFileUtil;
41 import com.intellij.vcsUtil.VcsUtil;
42 import git4idea.changes.GitChangeUtils;
43 import git4idea.changes.GitCommittedChangeList;
44 import git4idea.commands.GitCommand;
45 import git4idea.commands.GitHandler;
46 import git4idea.commands.GitSimpleHandler;
47 import git4idea.config.GitConfigUtil;
48 import git4idea.i18n.GitBundle;
49 import git4idea.repo.GitRemote;
50 import git4idea.repo.GitRepository;
51 import git4idea.repo.GitRepositoryManager;
52 import git4idea.util.GitUIUtil;
53 import git4idea.util.StringScanner;
54 import org.jetbrains.annotations.NotNull;
55 import org.jetbrains.annotations.Nullable;
58 import java.io.IOException;
59 import java.io.UnsupportedEncodingException;
60 import java.nio.charset.Charset;
64 * Git utility/helper methods
66 public class GitUtil {
68 * Comparator for virtual files by name
70 public static final Comparator<VirtualFile> VIRTUAL_FILE_COMPARATOR = new Comparator<VirtualFile>() {
71 public int compare(final VirtualFile o1, final VirtualFile o2) {
72 if (o1 == null && o2 == null) {
81 return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
85 * The UTF-8 encoding name
87 public static final String UTF8_ENCODING = "UTF-8";
91 public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
92 public static final String DOT_GIT = ".git";
94 private final static Logger LOG = Logger.getInstance(GitUtil.class);
95 private static final int SHORT_HASH_LENGTH = 8;
98 * A private constructor to suppress instance creation
105 public static VirtualFile findGitDir(@NotNull VirtualFile rootDir) {
106 VirtualFile child = rootDir.findChild(DOT_GIT);
110 if (child.isDirectory()) {
114 // this is standard for submodules, although probably it can
117 content = readFile(child);
119 catch (IOException e) {
120 throw new RuntimeException("Couldn't read " + child, e);
123 String prefix = "gitdir:";
124 if (content.startsWith(prefix)) {
125 pathToDir = content.substring(prefix.length()).trim();
131 if (!FileUtil.isAbsolute(pathToDir)) {
132 String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootDir.getPath(), pathToDir));
133 if (canonicalPath == null) {
136 pathToDir = FileUtil.toSystemIndependentName(canonicalPath);
138 return VcsUtil.getVirtualFile(pathToDir);
142 * Makes 3 attempts to get the contents of the file. If all 3 fail with an IOException, rethrows the exception.
145 public static String readFile(@NotNull VirtualFile file) throws IOException {
146 final int ATTEMPTS = 3;
147 for (int attempt = 0; attempt < ATTEMPTS; attempt++) {
149 return new String(file.contentsToByteArray());
151 catch (IOException e) {
152 LOG.info(String.format("IOException while reading %s (attempt #%s)", file, attempt));
153 if (attempt >= ATTEMPTS - 1) {
158 throw new AssertionError("Shouldn't get here. Couldn't read " + file);
162 * Sort files by Git root
164 * @param virtualFiles files to sort
165 * @return sorted files
166 * @throws VcsException if non git files are passed
169 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
170 return sortFilesByGitRoot(virtualFiles, false);
174 * Sort files by Git root
176 * @param virtualFiles files to sort
177 * @param ignoreNonGit if true, non-git files are ignored
178 * @return sorted files
179 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
181 public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
182 throws VcsException {
183 Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
184 for (VirtualFile file : virtualFiles) {
185 final VirtualFile vcsRoot = gitRootOrNull(file);
186 if (vcsRoot == null) {
191 throw new VcsException("The file " + file.getPath() + " is not under Git");
194 List<VirtualFile> files = result.get(vcsRoot);
196 files = new ArrayList<VirtualFile>();
197 result.put(vcsRoot, files);
205 * Sort files by vcs root
207 * @param files files to sort.
208 * @return the map from root to the files under the root
209 * @throws VcsException if non git files are passed
211 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
212 return sortFilePathsByGitRoot(files, false);
216 * Sort files by vcs root
218 * @param files files to sort.
219 * @return the map from root to the files under the root
221 public static Map<VirtualFile, List<FilePath>> sortGitFilePathsByGitRoot(Collection<FilePath> files) {
223 return sortFilePathsByGitRoot(files, true);
225 catch (VcsException e) {
226 throw new RuntimeException("Unexpected exception:", e);
232 * Sort files by vcs root
234 * @param files files to sort.
235 * @param ignoreNonGit if true, non-git files are ignored
236 * @return the map from root to the files under the root
237 * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
239 public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(Collection<FilePath> files, boolean ignoreNonGit)
240 throws VcsException {
241 Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
242 for (FilePath p : files) {
243 VirtualFile root = getGitRootOrNull(p);
249 throw new VcsException("The file " + p.getPath() + " is not under Git");
252 List<FilePath> l = rc.get(root);
254 l = new ArrayList<FilePath>();
263 * Parse UNIX timestamp as it is returned by the git
265 * @param value a value to parse
266 * @return timestamp as {@link Date} object
268 private static Date parseTimestamp(String value) {
270 parsed = Long.parseLong(value.trim());
271 return new Date(parsed * 1000);
275 * Parse UNIX timestamp returned from Git and handle {@link NumberFormatException} if one happens: return new {@link Date} and
276 * log the error properly.
277 * In some cases git output gets corrupted and this method is intended to catch the reason, why.
278 * @param value Value to parse.
279 * @param handler Git handler that was called to received the output.
280 * @param gitOutput Git output.
281 * @return Parsed Date or <code>new Date</code> in the case of error.
283 public static Date parseTimestampWithNFEReport(String value, GitHandler handler, String gitOutput) {
285 return parseTimestamp(value);
286 } catch (NumberFormatException e) {
287 LOG.error("annotate(). NFE. Handler: " + handler + ". Output: " + gitOutput, e);
293 * Get git roots from content roots
295 * @param roots git content roots
296 * @return a content root
298 public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
299 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
300 for (VirtualFile root : roots) {
301 VirtualFile f = root;
303 if (f.findFileByRelativePath(DOT_GIT) != null) {
315 * Return a git root for the file path (the parent directory with ".git" subdirectory)
317 * @param filePath a file path
318 * @return git root for the file
319 * @throws IllegalArgumentException if the file is not under git
320 * @throws VcsException if the file is not under git
322 * @deprecated because uses the java.io.File.
323 * @use GitRepositoryManager#getRepositoryForFile().
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
339 * @deprecated because uses the java.io.File.
340 * @use GitRepositoryManager#getRepositoryForFile().
344 public static VirtualFile getGitRootOrNull(final FilePath filePath) {
345 return getGitRootOrNull(filePath.getIOFile());
348 public static boolean isGitRoot(final File file) {
349 return file != null && file.exists() && file.isDirectory() && new File(file, DOT_GIT).exists();
353 * @deprecated because uses the java.io.File.
354 * @use GitRepositoryManager#getRepositoryForFile().
358 public static VirtualFile getGitRootOrNull(final File file) {
360 while (root != null && (!root.exists() || !root.isDirectory() || !new File(root, DOT_GIT).exists())) {
361 root = root.getParentFile();
363 return root == null ? null : LocalFileSystem.getInstance().findFileByIoFile(root);
367 * Return a git root for the file (the parent directory with ".git" subdirectory)
369 * @param file the file to check
370 * @return git root for the file
371 * @throws VcsException if the file is not under git
373 * @deprecated because uses the java.io.File.
374 * @use GitRepositoryManager#getRepositoryForFile().
376 public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
377 final VirtualFile root = gitRootOrNull(file);
382 throw new VcsException("The file " + file.getPath() + " is not under git.");
387 * Return a git root for the file (the parent directory with ".git" subdirectory)
389 * @param file the file to check
390 * @return git root for the file or null if the file is not not under Git
392 * @deprecated because uses the java.io.File.
393 * @use GitRepositoryManager#getRepositoryForFile().
396 public static VirtualFile gitRootOrNull(final VirtualFile file) {
397 if (file instanceof AbstractVcsVirtualFile) {
398 return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
400 VirtualFile root = file;
401 while (root != null) {
402 if (root.findFileByRelativePath(DOT_GIT) != null) {
405 root = root.getParent();
411 * Get git roots for the project. The method shows dialogs in the case when roots cannot be retrieved, so it should be called
412 * from the event dispatch thread.
414 * @param project the project
415 * @param vcs the git Vcs
416 * @return the list of the roots
418 * @deprecated because uses the java.io.File.
419 * @use GitRepositoryManager#getRepositoryForFile().
422 public static List<VirtualFile> getGitRoots(Project project, GitVcs vcs) throws VcsException {
423 final VirtualFile[] contentRoots = ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(vcs);
424 if (contentRoots == null || contentRoots.length == 0) {
425 throw new VcsException(GitBundle.getString("repository.action.missing.roots.unconfigured.message"));
427 final List<VirtualFile> roots = new ArrayList<VirtualFile>(gitRootsForPaths(Arrays.asList(contentRoots)));
428 if (roots.size() == 0) {
429 throw new VcsException(GitBundle.getString("repository.action.missing.roots.misconfigured"));
431 Collections.sort(roots, VIRTUAL_FILE_COMPARATOR);
437 * Check if the virtual file under git
439 * @param vFile a virtual file
440 * @return true if the file is under git
442 public static boolean isUnderGit(final VirtualFile vFile) {
443 return gitRootOrNull(vFile) != null;
448 * Return committer name based on author name and committer name
450 * @param authorName the name of author
451 * @param committerName the name of committer
452 * @return just a name if they are equal, or name that includes both author and committer
454 public static String adjustAuthorName(final String authorName, String committerName) {
455 if (!authorName.equals(committerName)) {
456 //noinspection HardCodedStringLiteral
457 committerName = authorName + ", via " + committerName;
459 return committerName;
463 * Check if the file path is under git
465 * @param path the path
466 * @return true if the file path is under git
468 public static boolean isUnderGit(final FilePath path) {
469 return getGitRootOrNull(path) != null;
473 * Get git roots for the selected paths
475 * @param filePaths the context paths
476 * @return a set of git roots
478 public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
479 HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
480 for (FilePath path : filePaths) {
481 final VirtualFile root = getGitRootOrNull(path);
490 * Get git time (UNIX time) basing on the date object
492 * @param time the time to convert
493 * @return the time in git format
495 public static String gitTime(Date time) {
496 long t = time.getTime() / 1000;
497 return Long.toString(t);
501 * Format revision number from long to 16-digit abbreviated revision
503 * @param rev the abbreviated revision number as long
504 * @return the revision string
506 public static String formatLongRev(long rev) {
507 return String.format("%015x%x", (rev >>> 4), rev & 0xF);
510 public static void getLocalCommittedChanges(final Project project,
511 final VirtualFile root,
512 final Consumer<GitSimpleHandler> parametersSpecifier,
513 final Consumer<GitCommittedChangeList> consumer, boolean skipDiffsForMerge) throws VcsException {
514 GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
517 h.addParameters("--pretty=format:%x04%x01" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
518 parametersSpecifier.consume(h);
520 String output = h.run();
521 LOG.debug("getLocalCommittedChanges output: '" + output + "'");
522 StringScanner s = new StringScanner(output);
523 final StringBuilder sb = new StringBuilder();
524 boolean firstStep = true;
525 while (s.hasMoreData()) {
526 final String line = s.line();
527 final boolean lineIsAStart = line.startsWith("\u0004\u0001");
528 if ((!firstStep) && lineIsAStart) {
529 final StringScanner innerScanner = new StringScanner(sb.toString());
531 consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
533 sb.append(lineIsAStart ? line.substring(2) : line).append('\n');
536 if (sb.length() > 0) {
537 final StringScanner innerScanner = new StringScanner(sb.toString());
539 consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
541 if (s.hasMoreData()) {
542 throw new IllegalStateException("More input is avaialble: " + s.line());
546 public static List<GitCommittedChangeList> getLocalCommittedChanges(final Project project,
547 final VirtualFile root,
548 final Consumer<GitSimpleHandler> parametersSpecifier)
549 throws VcsException {
550 final List<GitCommittedChangeList> rc = new ArrayList<GitCommittedChangeList>();
552 getLocalCommittedChanges(project, root, parametersSpecifier, new Consumer<GitCommittedChangeList>() {
553 public void consume(GitCommittedChangeList committedChangeList) {
554 rc.add(committedChangeList);
562 * <p>Unescape path returned by Git.</p>
564 * If there are quotes in the file name, Git not only escapes them, but also encloses the file name into quotes:
565 * <code>"\"quote"</code>
568 * If there are spaces in the file name, Git displays the name as is, without escaping spaces and without enclosing name in quotes.
571 * @param path a path to unescape
572 * @return unescaped path ready to be searched in the VFS or file system.
573 * @throws com.intellij.openapi.vcs.VcsException if the path in invalid
576 public static String unescapePath(@NotNull String path) throws VcsException {
577 final String QUOTE = "\"";
578 if (path.startsWith(QUOTE) && path.endsWith(QUOTE)) {
579 path = path.substring(1, path.length() - 1);
582 final int l = path.length();
583 StringBuilder rc = new StringBuilder(l);
584 for (int i = 0; i < path.length(); i++) {
585 char c = path.charAt(i);
587 //noinspection AssignmentToForLoopParameter
590 throw new VcsException("Unterminated escape sequence in the path: " + path);
592 final char e = path.charAt(i);
607 if (VcsFileUtil.isOctal(e)) {
608 // collect sequence of characters as a byte array.
611 for (int j = i; j < l;) {
612 if (VcsFileUtil.isOctal(path.charAt(j))) {
614 for (int k = 0; k < 3 && j < l && VcsFileUtil.isOctal(path.charAt(j)); k++) {
615 //noinspection AssignmentToForLoopParameter
619 if (j + 1 >= l || path.charAt(j) != '\\' || !VcsFileUtil.isOctal(path.charAt(j + 1))) {
622 //noinspection AssignmentToForLoopParameter
625 // convert to byte array
626 byte[] b = new byte[n];
629 if (VcsFileUtil.isOctal(path.charAt(i))) {
631 for (int k = 0; k < 3 && i < l && VcsFileUtil.isOctal(path.charAt(i)); k++) {
632 code = code * 8 + (path.charAt(i) - '0');
633 //noinspection AssignmentToForLoopParameter
638 if (i + 1 >= l || path.charAt(i) != '\\' || !VcsFileUtil.isOctal(path.charAt(i + 1))) {
641 //noinspection AssignmentToForLoopParameter
644 assert n == b.length;
645 // add them to string
646 final String encoding = GitConfigUtil.getFileNameEncoding();
648 rc.append(new String(b, encoding));
650 catch (UnsupportedEncodingException e1) {
651 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
655 throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
663 return rc.toString();
666 public static boolean justOneGitRepository(Project project) {
667 if (project.isDisposed()) {
670 GitRepositoryManager manager = getRepositoryManager(project);
671 if (manager == null) {
674 return !manager.moreThanOneRoot();
677 public static List<GitRepository> sortRepositories(@NotNull Collection<GitRepository> repositories) {
678 List<GitRepository> repos = new ArrayList<GitRepository>(repositories);
679 Collections.sort(repos, new Comparator<GitRepository>() {
680 @Override public int compare(GitRepository o1, GitRepository o2) {
681 return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
688 public static GitRemote findRemoteByName(@NotNull GitRepository repository, @Nullable String name) {
692 for (GitRemote remote : repository.getRemotes()) {
693 if (remote.getName().equals(name)) {
701 public static Pair<GitRemote, GitBranch> findMatchingRemoteBranch(GitRepository repository, GitBranch branch) throws VcsException {
705 Works like git push <remote>, where <remote> is the current branch's remote (or origin, if no
706 remote is configured for the current branch).
709 String remoteName = branch.getTrackedRemoteName(repository.getProject(), repository.getRoot());
711 if (remoteName == null) {
712 remote = findOrigin(repository.getRemotes());
714 remote = findRemoteByName(repository, remoteName);
716 if (remote == null) {
720 for (GitBranch remoteBranch : repository.getBranches().getRemoteBranches()) {
721 if (remoteBranch.getName().equals(remote.getName() + "/" + branch.getName())) {
722 return Pair.create(remote, remoteBranch);
729 private static GitRemote findOrigin(Collection<GitRemote> remotes) {
730 for (GitRemote remote : remotes) {
731 if (remote.getName().equals("origin")) {
738 public static boolean repoContainsRemoteBranch(@NotNull GitRepository repository, @NotNull GitBranch dest) {
739 return repository.getBranches().getRemoteBranches().contains(dest);
743 * Convert {@link GitBranch GitBranches} to their names, and remove remote HEAD pointers.
746 public static Collection<String> getBranchNamesWithoutRemoteHead(@NotNull Collection<GitBranch> branches) {
747 Collection<String> names = new ArrayList<String>(branches.size());
748 for (GitBranch branch : branches) {
749 if (!branch.isRemote() || !branch.getShortName().equals("HEAD")) {
750 names.add(branch.getName());
757 public static Collection<VirtualFile> getRootsFromRepositories(@NotNull Collection<GitRepository> repositories) {
758 Collection<VirtualFile> roots = new ArrayList<VirtualFile>(repositories.size());
759 for (GitRepository repository : repositories) {
760 roots.add(repository.getRoot());
766 public static Collection<GitRepository> getRepositoriesFromRoots(@NotNull GitRepositoryManager repositoryManager,
767 @NotNull Collection<VirtualFile> roots) {
768 Collection<GitRepository> repositories = new ArrayList<GitRepository>(roots.size());
769 for (VirtualFile root : roots) {
770 GitRepository repo = repositoryManager.getRepositoryForRoot(root);
772 LOG.error("Repository not found for root " + root);
775 repositories.add(repo);
782 * Returns absolute paths which have changed remotely comparing to the current branch, i.e. performs
783 * <code>git diff --name-only master..origin/master</code>
786 public static Collection<String> getPathsDiffBetweenRefs(@NotNull String beforeRef, @NotNull String afterRef, @NotNull Project project,
787 @NotNull VirtualFile root) throws VcsException {
788 final GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF);
789 diff.addParameters("--name-only", "--pretty=format:");
790 diff.addParameters(beforeRef + ".." + afterRef);
792 diff.setStdoutSuppressed(true);
793 diff.setStderrSuppressed(true);
794 diff.setSilent(true);
795 final String output = diff.run();
797 final Collection<String> remoteChanges = new HashSet<String>();
798 for (StringScanner s = new StringScanner(output); s.hasMoreData();) {
799 final String relative = s.line();
800 if (StringUtil.isEmptyOrSpaces(relative)) {
803 final String path = root.getPath() + "/" + unescapePath(relative);
804 remoteChanges.add(FilePathsHelper.convertPath(path));
806 return remoteChanges;
810 * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager},
811 * i.e. this works only for local changes.
812 * Paths can be absolute or relative to the repository.
813 * If a path is not in the local changes, it is ignored.
816 public static List<Change> convertPathsToChanges(@NotNull GitRepository repository,
817 @NotNull Collection<String> affectedPaths, boolean relativePaths) {
818 ChangeListManager changeListManager = ChangeListManager.getInstance(repository.getProject());
819 List<Change> affectedChanges = new ArrayList<Change>();
820 for (String path : affectedPaths) {
823 file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path));
826 file = VcsUtil.getVirtualFile(path);
830 Change change = changeListManager.getChange(file);
831 if (change != null) {
832 affectedChanges.add(change);
836 return affectedChanges;
840 public static GitRepositoryManager getRepositoryManager(@NotNull Project project) {
841 return ServiceManager.getService(project, GitRepositoryManager.class);
845 public static String getPrintableRemotes(@NotNull Collection<GitRemote> remotes) {
846 return StringUtil.join(remotes, new Function<GitRemote, String>() {
848 public String fun(GitRemote remote) {
849 return remote.getName() + ": [" + StringUtil.join(remote.getUrls(), ", ") + "]";
855 public static String getShortHash(@NotNull String hash) {
856 if (hash.length() == 0) return "";
857 if (hash.length() == 40) return hash.substring(0, SHORT_HASH_LENGTH);
858 if (hash.length() > 40) // revision string encoded with date too
860 return hash.substring(hash.indexOf("[") + 1, SHORT_HASH_LENGTH);
866 public static String fileOrFolder(@NotNull VirtualFile file) {
867 if (file.isDirectory()) {
876 * Show changes made in the specified revision.
878 * @param project the project
879 * @param revision the revision number
880 * @param file the file affected by the revision
881 * @param local pass true to let the diff be editable, i.e. making the revision "at the right" be a local (current) revision.
882 * pass false to let both sides of the diff be non-editable.
883 * @param revertable pass true to let "Revert" action be active.
885 public static void showSubmittedFiles(final Project project, final String revision, final VirtualFile file,
886 final boolean local, final boolean revertable) {
887 new Task.Backgroundable(project, GitBundle.message("changes.retrieving", revision)) {
888 public void run(@NotNull ProgressIndicator indicator) {
889 indicator.setIndeterminate(true);
891 VirtualFile vcsRoot = getGitRoot(file);
892 final CommittedChangeList changeList = GitChangeUtils.getRevisionChanges(project, vcsRoot, revision, true, local, revertable);
893 if (changeList != null) {
894 UIUtil.invokeLaterIfNeeded(new Runnable() {
896 AbstractVcsHelper.getInstance(project).showChangesListBrowser(changeList,
897 GitBundle.message("paths.affected.title", revision));
902 catch (final VcsException e) {
903 UIUtil.invokeLaterIfNeeded(new Runnable() {
905 GitUIUtil.showOperationError(project, e, "git show");