/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package git4idea.annotate; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.util.BackgroundTaskUtil; import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.history.VcsAbstractHistorySession; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsHistoryCache; import com.intellij.openapi.vcs.history.VcsFileRevisionEx; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.vfs.VcsFileSystem; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Interner; import com.intellij.vcs.AnnotationProviderEx; import com.intellij.vcs.log.VcsUser; import com.intellij.vcs.log.VcsUserRegistry; import com.intellij.vcsUtil.VcsUtil; import git4idea.GitFileRevision; import git4idea.GitRevisionNumber; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.annotate.GitFileAnnotation.LineInfo; import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import git4idea.history.GitHistoryProvider; import git4idea.history.GitHistoryUtils; import git4idea.i18n.GitBundle; import git4idea.util.StringScanner; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; public class GitAnnotationProvider implements AnnotationProviderEx { private final Project myProject; @NonNls private static final String SUBJECT_KEY = "summary"; @NonNls private static final String FILENAME_KEY = "filename"; @NonNls private static final String PREVIOUS_KEY = "previous"; @NonNls private static final String AUTHOR_KEY = "author"; @NonNls private static final String AUTHOR_EMAIL_KEY = "author-mail"; @NonNls private static final String COMMITTER_TIME_KEY = "committer-time"; private static final Logger LOG = Logger.getInstance(GitAnnotationProvider.class); @NotNull private final VcsHistoryCache myCache; @NotNull private final VcsUserRegistry myUserRegistry; public GitAnnotationProvider(@NotNull Project project) { myProject = project; myCache = ProjectLevelVcsManager.getInstance(myProject).getVcsHistoryCache(); myUserRegistry = ServiceManager.getService(project, VcsUserRegistry.class); } @Override public boolean isCaching() { return true; } @NotNull public FileAnnotation annotate(@NotNull VirtualFile file) throws VcsException { return annotate(file, null); } @NotNull public FileAnnotation annotate(@NotNull final VirtualFile file, @Nullable final VcsFileRevision revision) throws VcsException { if (file.isDirectory()) { throw new VcsException("Cannot annotate a directory"); } final FilePath currentFilePath = VcsUtil.getFilePath(file.getPath()); final FilePath realFilePath; if (revision == null) { realFilePath = GitHistoryUtils.getLastCommitName(myProject, currentFilePath); } else { realFilePath = ((VcsFileRevisionEx)revision).getPath(); } VcsRevisionNumber revisionNumber = revision != null ? revision.getRevisionNumber() : null; return annotate(realFilePath, revisionNumber, file); } @NotNull @Override public FileAnnotation annotate(@NotNull final FilePath path, @NotNull final VcsRevisionNumber revision) throws VcsException { GitFileRevision fileRevision = new GitFileRevision(myProject, path, (GitRevisionNumber)revision); VcsVirtualFile file = new VcsVirtualFile(path.getPath(), fileRevision, VcsFileSystem.getInstance()); return annotate(path, revision, file); } private static void setProgressIndicatorText(@Nullable String text) { final ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator(); if (progress != null) progress.setText(text); } @NotNull private GitFileAnnotation annotate(@NotNull final FilePath repositoryFilePath, @Nullable final VcsRevisionNumber revision, @NotNull final VirtualFile file) throws VcsException { GitVcs vcs = GitVcs.getInstance(myProject); assert vcs != null; VcsRevisionNumber actualRevision = revision != null ? revision : vcs.getDiffProvider().getCurrentRevision(file); if (actualRevision != null) { Object annotatedData = myCache.get(repositoryFilePath, GitVcs.getKey(), actualRevision); if (annotatedData instanceof CachedData) return restoreFromCache(file, actualRevision, (CachedData)annotatedData); } GitFileAnnotation fileAnnotation = doAnnotate(repositoryFilePath, actualRevision, file); if (actualRevision != null) { myCache.put(repositoryFilePath, GitVcs.getKey(), actualRevision, cacheData(fileAnnotation)); } return fileAnnotation; } @NotNull private GitFileAnnotation doAnnotate(@NotNull final FilePath repositoryFilePath, @Nullable final VcsRevisionNumber revision, @NotNull final VirtualFile file) throws VcsException { setProgressIndicatorText(GitBundle.message("computing.annotation", file.getName())); VirtualFile root = GitUtil.getGitRoot(repositoryFilePath); GitSimpleHandler h = new GitSimpleHandler(myProject, root, GitCommand.BLAME); h.setStdoutSuppressed(true); h.addParameters("--porcelain", "-l", "-t", "-w"); if (revision == null) { h.addParameters("HEAD"); } else { h.addParameters(revision.asString()); } h.endOptions(); h.addRelativePaths(repositoryFilePath); String output = h.run(); GitFileAnnotation fileAnnotation = parseAnnotations(revision, file, root, output); loadFileHistoryInBackground(fileAnnotation); return fileAnnotation; } private void loadFileHistoryInBackground(@NotNull GitFileAnnotation fileAnnotation) { List<VcsFileRevision> fileRevisions = BackgroundTaskUtil.computeInBackgroundAndTryWait( () -> { try { VirtualFile file = fileAnnotation.getFile(); FilePath filePath = VcsUtil.getFilePath(file); VcsRevisionNumber currentRevision = fileAnnotation.getCurrentRevision(); if (file.isInLocalFileSystem() || currentRevision == null) { return loadFileHistory(filePath); } else { return GitHistoryUtils.history(myProject, filePath, null, currentRevision); } } catch (VcsException e) { LOG.error(e); return null; } }, (revisions) -> { if (revisions == null) return; ApplicationManager.getApplication().invokeLater(() -> { GitFileAnnotation newFileAnnotation = new GitFileAnnotation(fileAnnotation); newFileAnnotation.setRevisions(revisions); fileAnnotation.reload(newFileAnnotation); }, myProject.getDisposed()); }, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS ); if (fileRevisions != null) { fileAnnotation.setRevisions(fileRevisions); } } @Nullable private List<VcsFileRevision> loadFileHistory(@NotNull FilePath filePath) throws VcsException { GitVcs vcs = GitVcs.getInstance(myProject); if (vcs == null) return null; GitHistoryProvider historyProvider = vcs.getVcsHistoryProvider(); VcsAbstractHistorySession cachedSession = myCache.getFull(filePath, vcs.getKeyInstanceMethod(), historyProvider); if (cachedSession != null && !ContainerUtil.isEmpty(cachedSession.getRevisionList())) { return cachedSession.getRevisionList(); } else { VcsAbstractHistorySession session = historyProvider.createSessionFor(filePath); if (session == null) return null; myCache.put(filePath, null, vcs.getKeyInstanceMethod(), session, historyProvider, true); return session.getRevisionList(); } } @NotNull private GitFileAnnotation parseAnnotations(@Nullable VcsRevisionNumber revision, @NotNull VirtualFile file, @NotNull VirtualFile root, @NotNull String output) throws VcsException { Interner<FilePath> pathInterner = new Interner<>(); try { List<LineInfo> lines = new ArrayList<>(); HashMap<String, LineInfo> commits = new HashMap<>(); for (StringScanner s = new StringScanner(output); s.hasMoreData(); ) { // parse header line String commitHash = s.spaceToken(); if (commitHash.equals(GitRevisionNumber.NOT_COMMITTED_HASH)) { commitHash = null; } s.spaceToken(); // skip revision line number String s1 = s.spaceToken(); int lineNum = Integer.parseInt(s1); s.nextLine(); // parse commit information LineInfo commit = commits.get(commitHash); if (commit != null || commitHash == null) { while (s.hasMoreData() && !s.startsWith('\t')) { s.nextLine(); } } else { Date committerDate = null; FilePath filePath = null; String subject = null; String authorName = null; String authorEmail = null; String previousRevision = null; FilePath previousFilePath = null; while (s.hasMoreData() && !s.startsWith('\t')) { String key = s.spaceToken(); String value = s.line(); if (SUBJECT_KEY.equals(key)) { subject = value; } else if (AUTHOR_KEY.equals(key)) { authorName = value; } else if (COMMITTER_TIME_KEY.equals(key)) { committerDate = GitUtil.parseTimestamp(value); } else if (FILENAME_KEY.equals(key)) { filePath = VcsUtil.getFilePath(root, value); } else if (AUTHOR_EMAIL_KEY.equals(key)) { authorEmail = value; if (authorEmail.startsWith("<") && authorEmail.endsWith(">")) { authorEmail = authorEmail.substring(1, authorEmail.length() - 1); } } else if (PREVIOUS_KEY.equals(key)) { int index = value.indexOf(' '); if (index != -1) { previousRevision = value.substring(0, index); previousFilePath = VcsUtil.getFilePath(root, value.substring(index + 1, value.length())); } } } if (committerDate == null || filePath == null || authorName == null || authorEmail == null || subject == null) { throw new VcsException("Output for line " + lineNum + " lacks necessary data"); } GitRevisionNumber revisionNumber = new GitRevisionNumber(commitHash, committerDate); VcsUser author = myUserRegistry.createUser(authorName, authorEmail); GitRevisionNumber previousRevisionNumber = previousRevision != null ? new GitRevisionNumber(previousRevision) : null; filePath = pathInterner.intern(filePath); if (previousFilePath != null) previousFilePath = pathInterner.intern(previousFilePath); commit = new LineInfo(myProject, revisionNumber, filePath, committerDate, author, subject, previousRevisionNumber, previousFilePath); commits.put(commitHash, commit); } s.nextLine(); int expectedLineNum = lines.size() + 1; if (lineNum != expectedLineNum) { throw new VcsException("Adding for info for line " + lineNum + " but we are expecting it to be for " + expectedLineNum); } lines.add(commit); } return new GitFileAnnotation(myProject, file, revision, lines); } catch (Exception e) { LOG.error("Couldn't parse annotation: " + e, new Attachment("output.txt", output)); throw new VcsException(e); } } @NotNull private GitFileAnnotation restoreFromCache(@NotNull VirtualFile file, @Nullable VcsRevisionNumber revisionNumber, @NotNull CachedData data) { GitFileAnnotation fileAnnotation = new GitFileAnnotation(myProject, file, revisionNumber, data.lines); loadFileHistoryInBackground(fileAnnotation); return fileAnnotation; } @NotNull private CachedData cacheData(@NotNull GitFileAnnotation annotation) { return new CachedData(annotation.getLines()); } private static class CachedData { public final List<LineInfo> lines; public CachedData(List<LineInfo> lines) { this.lines = lines; } } }