From a7c864a9f4b8f0165d28168eded83a6f8bd0f900 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 29 Aug 2016 16:42:36 +0300 Subject: [PATCH] git: annotations - speedup loading * load file history asynchronously * cache loaded annotations * do not store annotated content - load it on demand * use `git blame` output as much as we can * the only usage for file history ATM - `HistoryIdColumn`, as we can't properly order commits without it --- .../util/containers/ContainerUtil.java | 6 + .../openapi/vcs/annotate/FileAnnotation.java | 151 ++++++++++- .../vcs/history/VcsAnnotationCachedProxy.java | 8 +- .../openapi/vcs/history/VcsHistoryCache.java | 9 +- .../AnnotateCurrentRevisionAction.java | 39 ++- .../AnnotatePreviousRevisionAction.java | 53 ++-- .../vcs/actions/AnnotateRevisionAction.java | 19 +- .../vcs/actions/AnnotateToggleAction.java | 65 +++-- plugins/git4idea/src/git4idea/GitVcs.java | 7 +- .../annotate/GitAnnotationProvider.java | 227 ++++++++++++---- .../git4idea/annotate/GitFileAnnotation.java | 242 +++++++++++++----- .../git4idea/history/GitHistoryProvider.java | 2 +- 12 files changed, 604 insertions(+), 224 deletions(-) diff --git a/platform/util/src/com/intellij/util/containers/ContainerUtil.java b/platform/util/src/com/intellij/util/containers/ContainerUtil.java index b6c63640a630..8e40d738099b 100644 --- a/platform/util/src/com/intellij/util/containers/ContainerUtil.java +++ b/platform/util/src/com/intellij/util/containers/ContainerUtil.java @@ -2906,6 +2906,12 @@ public class ContainerUtil extends ContainerUtilRt { protected Map> createMap(int initialCapacity, float loadFactor) { return new TreeMap>(); } + + @NotNull + public NavigableSet navigableKeySet() { + //noinspection unchecked + return ((TreeMap)myMap).navigableKeySet(); + } } @NotNull diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/FileAnnotation.java b/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/FileAnnotation.java index 46d0b6f430c4..36bace83a5c4 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/FileAnnotation.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/FileAnnotation.java @@ -15,6 +15,7 @@ */ package com.intellij.openapi.vcs.annotate; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.localVcs.UpToDateLineNumberProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsKey; @@ -23,20 +24,24 @@ import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Date; -import java.util.List; +import java.util.*; /** * Represents annotations ("vcs blame") for some file in a specific revision * @see AnnotationProvider */ public abstract class FileAnnotation { + private static final Logger LOG = Logger.getInstance(FileAnnotation.class); + @NotNull private final Project myProject; private Runnable myCloser; + private Runnable myReloader; protected FileAnnotation(@NotNull Project project) { myProject = project; @@ -174,6 +179,13 @@ public abstract class FileAnnotation { myCloser.run(); } + /** + * Notify that annotation information has changed, and UI should be updated. + */ + public final void reload() { + if (myReloader != null) myReloader.run(); + } + /** * @see #close() */ @@ -181,9 +193,144 @@ public abstract class FileAnnotation { myCloser = closer; } + /** + * @see #reload() + */ + public final void setReloader(@Nullable Runnable reloader) { + myReloader = reloader; + } + @Deprecated public boolean revisionsNotEmpty() { return true; } + + + @Nullable + public CurrentFileRevisionProvider getCurrentFileRevisionProvider() { + return createDefaultCurrentFileRevisionProvider(this); + } + + @Nullable + public PreviousFileRevisionProvider getPreviousFileRevisionProvider() { + return createDefaultPreviousFileRevisionProvider(this); + } + + @Nullable + public AuthorsMappingProvider getAuthorsMappingProvider() { + return createDefaultAuthorsMappingProvider(this); + } + + @Nullable + public RevisionsOrderProvider getRevisionsOrderProvider() { + return createDefaultRevisionsOrderProvider(this); + } + + + public interface CurrentFileRevisionProvider { + @Nullable + VcsFileRevision getRevision(int lineNumber); + } + + public interface PreviousFileRevisionProvider { + @Nullable + VcsFileRevision getPreviousRevision(int lineNumber); + + @Nullable + VcsFileRevision getLastRevision(); + } + + public interface AuthorsMappingProvider { + @NotNull + Map getAuthors(); + } + + public interface RevisionsOrderProvider { + @NotNull + List> getOrderedRevisions(); + } + + + @Nullable + private static CurrentFileRevisionProvider createDefaultCurrentFileRevisionProvider(@NotNull FileAnnotation annotation) { + List revisions = annotation.getRevisions(); + if (revisions == null) return null; + + + Map map = new HashMap<>(); + for (VcsFileRevision revision : revisions) { + map.put(revision.getRevisionNumber(), revision); + } + + List lineToRevision = new ArrayList<>(annotation.getLineCount()); + for (int i = 0; i < annotation.getLineCount(); i++) { + lineToRevision.add(map.get(annotation.getLineRevisionNumber(i))); + } + + return (lineNumber) -> { + LOG.assertTrue(lineNumber >= 0 && lineNumber < lineToRevision.size()); + return lineToRevision.get(lineNumber); + }; + } + + @Nullable + private static PreviousFileRevisionProvider createDefaultPreviousFileRevisionProvider(@NotNull FileAnnotation annotation) { + List revisions = annotation.getRevisions(); + if (revisions == null) return null; + + Map map = new HashMap<>(); + for (int i = 0; i < revisions.size(); i++) { + VcsFileRevision revision = revisions.get(i); + VcsFileRevision previousRevision = i + 1 < revisions.size() ? revisions.get(i + 1) : null; + map.put(revision.getRevisionNumber(), previousRevision); + } + + List lineToRevision = new ArrayList<>(annotation.getLineCount()); + for (int i = 0; i < annotation.getLineCount(); i++) { + lineToRevision.add(map.get(annotation.getLineRevisionNumber(i))); + } + + VcsFileRevision lastRevision = ContainerUtil.getFirstItem(revisions); + + return new PreviousFileRevisionProvider() { + @Nullable + @Override + public VcsFileRevision getPreviousRevision(int lineNumber) { + LOG.assertTrue(lineNumber >= 0 && lineNumber < lineToRevision.size()); + return lineToRevision.get(lineNumber); + } + + @Nullable + @Override + public VcsFileRevision getLastRevision() { + return lastRevision; + } + }; + } + + @Nullable + private static AuthorsMappingProvider createDefaultAuthorsMappingProvider(@NotNull FileAnnotation annotation) { + List revisions = annotation.getRevisions(); + if (revisions == null) return null; + + Map authorsMapping = new HashMap<>(); + for (VcsFileRevision revision : revisions) { + authorsMapping.put(revision.getRevisionNumber(), revision.getAuthor()); + } + + return () -> authorsMapping; + } + + @Nullable + private static RevisionsOrderProvider createDefaultRevisionsOrderProvider(@NotNull FileAnnotation annotation) { + List revisions = annotation.getRevisions(); + if (revisions == null) return null; + + List> orderedRevisions = ContainerUtil.map(revisions, (revision) -> { + return Collections.singletonList(revision.getRevisionNumber()); + }); + + return () -> orderedRevisions; + } } diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsAnnotationCachedProxy.java b/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsAnnotationCachedProxy.java index 2c9630bae2d2..17a58af73c1c 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsAnnotationCachedProxy.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsAnnotationCachedProxy.java @@ -23,7 +23,6 @@ import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.actions.VcsContextFactory; import com.intellij.openapi.vcs.annotate.AnnotationProvider; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.annotate.VcsAnnotation; @@ -31,6 +30,8 @@ import com.intellij.openapi.vcs.annotate.VcsCacheableAnnotationProvider; import com.intellij.openapi.vcs.changes.ContentRevision; import com.intellij.openapi.vcs.diff.DiffProvider; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ObjectUtils; +import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -87,13 +88,14 @@ public class VcsAnnotationCachedProxy implements AnnotationProvider { final ThrowableComputable delegate) throws VcsException { final AnnotationProvider annotationProvider = myAnnotationProvider; - final FilePath filePath = VcsContextFactory.SERVICE.getInstance().createFilePathOn(file); + final FilePath filePath = VcsUtil.getFilePath(file); final VcsCacheableAnnotationProvider cacheableAnnotationProvider = (VcsCacheableAnnotationProvider)annotationProvider; VcsAnnotation vcsAnnotation = null; if (revisionNumber != null) { - vcsAnnotation = myCache.get(VcsContextFactory.SERVICE.getInstance().createFilePathOn(file), myVcs.getKeyInstanceMethod(), revisionNumber); + Object cachedData = myCache.get(filePath, myVcs.getKeyInstanceMethod(), revisionNumber); + vcsAnnotation = ObjectUtils.tryCast(cachedData, VcsAnnotation.class); } if (vcsAnnotation != null) { diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsHistoryCache.java b/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsHistoryCache.java index c18b6bf9cbaf..3665a7bd24db 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsHistoryCache.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/history/VcsHistoryCache.java @@ -18,7 +18,6 @@ package com.intellij.openapi.vcs.history; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsKey; -import com.intellij.openapi.vcs.annotate.VcsAnnotation; import com.intellij.util.Consumer; import com.intellij.util.containers.SLRUMap; import org.jetbrains.annotations.NotNull; @@ -37,8 +36,7 @@ import java.util.Map; public class VcsHistoryCache { private final Object myLock; private final SLRUMap myHistoryCache; - private final SLRUMap myAnnotationCache; - //private final SLRUMap myContentCache; + private final SLRUMap myAnnotationCache; public VcsHistoryCache() { myLock = new Object(); @@ -50,7 +48,6 @@ public class VcsHistoryCache { myAnnotationCache = new SLRUMap<>( preloadEnabled ? 50 : 10, preloadEnabled ? 50 : 5); - //myContentCache = new SLRUMap(20, 20); } public void put(final FilePath filePath, @@ -114,13 +111,13 @@ public class VcsHistoryCache { } public void put(@NotNull final FilePath filePath, @NotNull final VcsKey vcsKey, @NotNull final VcsRevisionNumber number, - @NotNull final VcsAnnotation vcsAnnotation) { + @NotNull final Object vcsAnnotation) { synchronized (myLock) { myAnnotationCache.put(new HistoryCacheWithRevisionKey(filePath, vcsKey, number), vcsAnnotation); } } - public VcsAnnotation get(@NotNull final FilePath filePath, @NotNull final VcsKey vcsKey, @NotNull final VcsRevisionNumber number) { + public Object get(@NotNull final FilePath filePath, @NotNull final VcsKey vcsKey, @NotNull final VcsRevisionNumber number) { synchronized (myLock) { return myAnnotationCache.get(new HistoryCacheWithRevisionKey(filePath, vcsKey, number)); } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateCurrentRevisionAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateCurrentRevisionAction.java index 3ba999993a6f..0103ef58d28f 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateCurrentRevisionAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateCurrentRevisionAction.java @@ -1,44 +1,35 @@ package com.intellij.openapi.vcs.actions; import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.history.VcsFileRevision; -import com.intellij.openapi.vcs.history.VcsRevisionNumber; -import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - class AnnotateCurrentRevisionAction extends AnnotateRevisionAction { - @Nullable private final List myRevisions; + @Nullable private final FileAnnotation.CurrentFileRevisionProvider myProvider; public AnnotateCurrentRevisionAction(@NotNull FileAnnotation annotation, @NotNull AbstractVcs vcs) { super("Annotate Revision", "Annotate selected revision in new tab", AllIcons.Actions.Annotate, annotation, vcs); - List revisions = annotation.getRevisions(); - if (revisions == null) { - myRevisions = null; - return; - } - - Map map = new HashMap<>(); - for (VcsFileRevision revision : revisions) { - map.put(revision.getRevisionNumber(), revision); - } - - myRevisions = new ArrayList<>(annotation.getLineCount()); - for (int i = 0; i < annotation.getLineCount(); i++) { - myRevisions.add(map.get(annotation.getLineRevisionNumber(i))); - } + myProvider = annotation.getCurrentFileRevisionProvider(); } @Override + public void update(@NotNull AnActionEvent e) { + if (myProvider == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + super.update(e); + } + @Nullable - public List getRevisions() { - return myRevisions; + @Override + protected VcsFileRevision getRevision(int lineNumber) { + assert myProvider != null; + return myProvider.getRevision(lineNumber); } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotatePreviousRevisionAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotatePreviousRevisionAction.java index bc05475268cf..dba8837c48da 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotatePreviousRevisionAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotatePreviousRevisionAction.java @@ -6,57 +6,36 @@ import com.intellij.openapi.localVcs.UpToDateLineNumberProvider; import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.history.VcsFileRevision; -import com.intellij.openapi.vcs.history.VcsRevisionNumber; -import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - class AnnotatePreviousRevisionAction extends AnnotateRevisionAction { - @Nullable private final List myRevisions; - @Nullable private final VcsFileRevision myLastRevision; + @Nullable private final FileAnnotation.PreviousFileRevisionProvider myProvider; public AnnotatePreviousRevisionAction(@NotNull FileAnnotation annotation, @NotNull AbstractVcs vcs) { super("Annotate Previous Revision", "Annotate successor of selected revision in new tab", AllIcons.Actions.Annotate, annotation, vcs); - List revisions = annotation.getRevisions(); - if (revisions == null) { - myRevisions = null; - myLastRevision = null; + myProvider = annotation.getPreviousFileRevisionProvider(); + } + + @Override + public void update(@NotNull AnActionEvent e) { + if (myProvider == null) { + e.getPresentation().setEnabledAndVisible(false); return; } - - Map map = new HashMap<>(); - for (int i = 0; i < revisions.size(); i++) { - VcsFileRevision revision = revisions.get(i); - VcsFileRevision previousRevision = i + 1 < revisions.size() ? revisions.get(i + 1) : null; - map.put(revision.getRevisionNumber(), previousRevision); - } - - myRevisions = new ArrayList<>(annotation.getLineCount()); - for (int i = 0; i < annotation.getLineCount(); i++) { - myRevisions.add(map.get(annotation.getLineRevisionNumber(i))); - } - - myLastRevision = ContainerUtil.getFirstItem(revisions); - } - - @Override - @Nullable - public List getRevisions() { - return myRevisions; + super.update(e); } @Nullable @Override - protected VcsFileRevision getFileRevision(@NotNull AnActionEvent e) { - if (getCurrentLine() == UpToDateLineNumberProvider.ABSENT_LINE_NUMBER) { - return myLastRevision; + protected VcsFileRevision getRevision(int lineNumber) { + assert myProvider != null; + if (lineNumber == UpToDateLineNumberProvider.ABSENT_LINE_NUMBER) { + return myProvider.getLastRevision(); + } + else { + return myProvider.getPreviousRevision(lineNumber); } - return super.getFileRevision(e); } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateRevisionAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateRevisionAction.java index 033601517726..c51fb9d2e9d3 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateRevisionAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateRevisionAction.java @@ -21,7 +21,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.List; abstract class AnnotateRevisionAction extends AnnotateRevisionActionBase implements DumbAware, UpToDateLineNumberListener { @NotNull private final FileAnnotation myAnnotation; @@ -48,17 +47,12 @@ abstract class AnnotateRevisionAction extends AnnotateRevisionActionBase impleme return; } - if (getRevisions() == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } e.getPresentation().setVisible(true); - super.update(e); } @Nullable - protected abstract List getRevisions(); + protected abstract VcsFileRevision getRevision(int lineNumber); @Nullable protected AbstractVcs getVcs(@NotNull AnActionEvent e) { @@ -89,11 +83,8 @@ abstract class AnnotateRevisionAction extends AnnotateRevisionActionBase impleme @Nullable @Override protected VcsFileRevision getFileRevision(@NotNull AnActionEvent e) { - List revisions = getRevisions(); - assert revisions != null; - - if (currentLine < 0 || currentLine >= revisions.size()) return null; - return revisions.get(currentLine); + if (currentLine < 0 || currentLine >= myAnnotation.getLineCount()) return null; + return getRevision(currentLine); } @Override @@ -112,8 +103,4 @@ abstract class AnnotateRevisionAction extends AnnotateRevisionActionBase impleme public void consume(Integer integer) { currentLine = integer; } - - public int getCurrentLine() { - return currentLine; - } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java index 97e55da7fb97..dee74d044254 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java @@ -87,7 +87,7 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware { @Nullable final VirtualFile currentFile, @NotNull final FileAnnotation fileAnnotation, @NotNull final AbstractVcs vcs, - @Nullable UpToDateLineNumberProvider getUpToDateLineNumber) { + @Nullable final UpToDateLineNumberProvider upToDateLineNumberProvider) { if (fileAnnotation.getFile() != null && fileAnnotation.getFile().isInLocalFileSystem()) { ProjectLevelVcsManager.getInstance(project).getAnnotationLocalChangesListener().registerAnnotation(fileAnnotation.getFile(), fileAnnotation); } @@ -101,11 +101,18 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware { }); }); + fileAnnotation.setReloader(() -> { + if (editor.getGutter().isAnnotationsShown()) { + doAnnotate(editor, project, currentFile, fileAnnotation, vcs, upToDateLineNumberProvider); + } + }); final EditorGutterComponentEx editorGutter = (EditorGutterComponentEx)editor.getGutter(); final List gutters = new ArrayList<>(); final AnnotationSourceSwitcher switcher = fileAnnotation.getAnnotationSourceSwitcher(); - if (getUpToDateLineNumber == null) getUpToDateLineNumber = new UpToDateLineNumberProviderImpl(editor.getDocument(), project); + UpToDateLineNumberProvider getUpToDateLineNumber = upToDateLineNumberProvider != null ? + upToDateLineNumberProvider : + new UpToDateLineNumberProviderImpl(editor.getDocument(), project); final AnnotationPresentation presentation = new AnnotationPresentation(fileAnnotation, getUpToDateLineNumber, switcher); if (currentFile != null && vcs.getCommittedChangesProvider() != null) { @@ -194,39 +201,47 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware { @Nullable private static Couple> computeBgColors(@NotNull FileAnnotation fileAnnotation, @NotNull Editor editor) { - final List fileRevisionList = fileAnnotation.getRevisions(); - if (ContainerUtil.isEmpty(fileRevisionList)) return null; - - final Map commitOrderColors = new HashMap<>(); - final Map commitAuthorColors = new HashMap<>(); - final Map authorColors = new HashMap<>(); + Map commitOrderColors = new HashMap<>(); + Map commitAuthorColors = new HashMap<>(); EditorColorsScheme colorScheme = editor.getColorsScheme(); AnnotationsSettings settings = AnnotationsSettings.getInstance(); List authorsColorPalette = settings.getAuthorsColors(colorScheme); List orderedColorPalette = settings.getOrderedColors(colorScheme); - final int revisionsCount = fileRevisionList.size(); - for (int i = 0; i < fileRevisionList.size(); i++) { - VcsFileRevision revision = fileRevisionList.get(i); - final VcsRevisionNumber number = revision.getRevisionNumber(); - final String author = revision.getAuthor(); - if (number == null) continue; + FileAnnotation.AuthorsMappingProvider authorsMappingProvider = fileAnnotation.getAuthorsMappingProvider(); + if (authorsMappingProvider != null) { + Map authorsMap = authorsMappingProvider.getAuthors(); - if (!commitAuthorColors.containsKey(number)) { - if (author != null && !authorColors.containsKey(author)) { - final int index = authorColors.size(); - Color color = authorsColorPalette.get(index % authorsColorPalette.size()); - authorColors.put(author, color); - } - - commitAuthorColors.put(number, authorColors.get(author)); + Map authorColors = new HashMap<>(); + for (String author : ContainerUtil.newTreeSet(authorsMap.values())) { + int index = authorColors.size(); + Color color = authorsColorPalette.get(index % authorsColorPalette.size()); + authorColors.put(author, color); } - if (!commitOrderColors.containsKey(number)) { - Color color = orderedColorPalette.get(orderedColorPalette.size() * i / revisionsCount); - commitOrderColors.put(number, color); + + for (Map.Entry entry : authorsMap.entrySet()) { + VcsRevisionNumber revision = entry.getKey(); + String author = entry.getValue(); + Color color = authorColors.get(author); + commitAuthorColors.put(revision, color); } } + + FileAnnotation.RevisionsOrderProvider revisionsOrderProvider = fileAnnotation.getRevisionsOrderProvider(); + if (revisionsOrderProvider != null) { + List> orderedRevisions = revisionsOrderProvider.getOrderedRevisions(); + + int revisionsCount = orderedRevisions.size(); + for (int index = 0; index < revisionsCount; index++) { + Color color = orderedColorPalette.get(orderedColorPalette.size() * index / revisionsCount); + + for (VcsRevisionNumber number : orderedRevisions.get(index)) { + commitOrderColors.put(number, color); + } + } + } + return Couple.of(commitOrderColors.size() > 1 ? commitOrderColors : null, commitAuthorColors.size() > 1 ? commitAuthorColors : null); } diff --git a/plugins/git4idea/src/git4idea/GitVcs.java b/plugins/git4idea/src/git4idea/GitVcs.java index 0bb7a927f2c1..679fd4736cb2 100644 --- a/plugins/git4idea/src/git4idea/GitVcs.java +++ b/plugins/git4idea/src/git4idea/GitVcs.java @@ -36,7 +36,6 @@ import com.intellij.openapi.vcs.changes.CommitExecutor; import com.intellij.openapi.vcs.checkin.CheckinEnvironment; import com.intellij.openapi.vcs.diff.DiffProvider; import com.intellij.openapi.vcs.diff.RevisionSelector; -import com.intellij.openapi.vcs.history.VcsHistoryProvider; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.merge.MergeProvider; import com.intellij.openapi.vcs.rollback.RollbackEnvironment; @@ -102,7 +101,7 @@ public class GitVcs extends AbstractVcs { private final GitUpdateEnvironment myUpdateEnvironment; private final GitAnnotationProvider myAnnotationProvider; private final DiffProvider myDiffProvider; - private final VcsHistoryProvider myHistoryProvider; + private final GitHistoryProvider myHistoryProvider; @NotNull private final Git myGit; private final ProjectLevelVcsManager myVcsManager; private final GitVcsApplicationSettings myAppSettings; @@ -203,12 +202,12 @@ public class GitVcs extends AbstractVcs { @Override @NotNull - public VcsHistoryProvider getVcsHistoryProvider() { + public GitHistoryProvider getVcsHistoryProvider() { return myHistoryProvider; } @Override - public VcsHistoryProvider getVcsBlockHistoryProvider() { + public GitHistoryProvider getVcsBlockHistoryProvider() { return myHistoryProvider; } diff --git a/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java b/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java index 9fc1b59ae12a..2eb5b138dd8b 100644 --- a/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java +++ b/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java @@ -15,30 +15,38 @@ */ 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.project.Project; -import com.intellij.openapi.util.Pair; 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.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.Function; 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; @@ -46,54 +54,66 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +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); } - public FileAnnotation annotate(@NotNull final VirtualFile file, final VcsFileRevision revision) throws VcsException { + @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; - setProgressIndicatorText(GitBundle.message("getting.history", file.getName())); - final List revisions = GitHistoryUtils.history(myProject, currentFilePath); if (revision == null) { realFilePath = GitHistoryUtils.getLastCommitName(myProject, currentFilePath); } else { realFilePath = ((GitFileRevision)revision).getPath(); } - setProgressIndicatorText(GitBundle.message("computing.annotation", file.getName())); VcsRevisionNumber revisionNumber = revision != null ? revision.getRevisionNumber() : null; - return annotate(realFilePath, revisionNumber, revisions, file); + return annotate(realFilePath, revisionNumber, file); } @NotNull @Override public FileAnnotation annotate(@NotNull final FilePath path, @NotNull final VcsRevisionNumber revision) throws VcsException { - setProgressIndicatorText(GitBundle.message("getting.history", path.getName())); - List revisions = GitHistoryUtils.history(myProject, path, null, revision); - GitFileRevision fileRevision = new GitFileRevision(myProject, path, (GitRevisionNumber)revision); VcsVirtualFile file = new VcsVirtualFile(path.getPath(), fileRevision, VcsFileSystem.getInstance()); - setProgressIndicatorText(GitBundle.message("computing.annotation", path.getName())); - return annotate(path, revision, revisions, file); + return annotate(path, revision, file); } private static void setProgressIndicatorText(@Nullable String text) { @@ -101,11 +121,36 @@ public class GitAnnotationProvider implements AnnotationProviderEx { if (progress != null) progress.setText(text); } + @NotNull private GitFileAnnotation annotate(@NotNull final FilePath repositoryFilePath, @Nullable final VcsRevisionNumber revision, - @NotNull final List revisions, @NotNull final VirtualFile file) throws VcsException { - GitSimpleHandler h = new GitSimpleHandler(myProject, GitUtil.getGitRoot(repositoryFilePath), GitCommand.BLAME); + 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, revision, (CachedData)annotatedData); + } + + GitFileAnnotation fileAnnotation = doAnnotate(repositoryFilePath, revision, 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.setCharset(file.getCharset()); h.addParameters("--porcelain", "-l", "-t", "-w"); @@ -118,19 +163,71 @@ public class GitAnnotationProvider implements AnnotationProviderEx { h.endOptions(); h.addRelativePaths(repositoryFilePath); String output = h.run(); - return parseAnnotations(revision, file, output, revisions); + + GitFileAnnotation fileAnnotation = parseAnnotations(revision, file, root, output); + + loadFileHistoryInBackground(fileAnnotation); + + return fileAnnotation; + } + + private void loadFileHistoryInBackground(@NotNull GitFileAnnotation fileAnnotation) { + ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + VirtualFile file = fileAnnotation.getFile(); + FilePath filePath = VcsUtil.getFilePath(file); + VcsRevisionNumber currentRevision = fileAnnotation.getCurrentRevision(); + + List revisions; + if (file.isInLocalFileSystem() || currentRevision == null) { + revisions = loadFileHistory(filePath); + } + else { + revisions = GitHistoryUtils.history(myProject, filePath, null, currentRevision); + } + + if (revisions == null) return; + ApplicationManager.getApplication().invokeLater(() -> { + fileAnnotation.setRevisions(revisions); + fileAnnotation.reload(); + }); + } + catch (VcsException e) { + LOG.error(e); + } + }); + } + + @Nullable + private List 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 String output, - @NotNull List revisions) throws VcsException { + @NotNull VirtualFile root, + @NotNull String output) throws VcsException { + Interner pathInterner = new Interner<>(); + try { - StringBuilder content = new StringBuilder(); List lines = new ArrayList<>(); HashMap commits = new HashMap<>(); - final Map historyAsMap = getRevisionMap(revisions); for (StringScanner s = new StringScanner(output); s.hasMoreData(); ) { // parse header line String commitHash = s.spaceToken(); @@ -149,41 +246,70 @@ public class GitAnnotationProvider implements AnnotationProviderEx { } } else { - GitRevisionNumber revisionNumber = null; Date committerDate = null; - String author = 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 (AUTHOR_KEY.equals(key)) { - author = value; + if (SUBJECT_KEY.equals(key)) { + subject = value; } - if (COMMITTER_TIME_KEY.equals(key)) { + else if (AUTHOR_KEY.equals(key)) { + authorName = value; + } + else if (COMMITTER_TIME_KEY.equals(key)) { committerDate = GitUtil.parseTimestamp(value); - revisionNumber = new GitRevisionNumber(commitHash, committerDate); + } + 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())); + } } } - commit = new LineInfo(committerDate, revisionNumber, (GitFileRevision)historyAsMap.get(revisionNumber), author); + + 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); } - // parse line - if (!s.hasMoreData()) { - // if the file is empty, the next line will not start with tab and it will be - // empty. - continue; - } - s.skipChars(1); + 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); } - content.append(s.line(true)); lines.add(commit); } - return new GitFileAnnotation(myProject, file, revision, content.toString(), lines, revisions); + return new GitFileAnnotation(myProject, file, revision, lines); } catch (Exception e) { LOG.error("Couldn't parse annotation: " + e, new Attachment("output.txt", output)); @@ -192,13 +318,26 @@ public class GitAnnotationProvider implements AnnotationProviderEx { } @NotNull - private static Map getRevisionMap(@NotNull List revisions) { - return ContainerUtil.map2Map(revisions, - new Function>() { - @Override - public Pair fun(VcsFileRevision revision) { - return Pair.create(revision.getRevisionNumber(), revision); - } - }); + 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 lines; + + public CachedData(List lines) { + this.lines = lines; + } } } diff --git a/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java b/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java index 336d06e345d0..8ca02c145245 100644 --- a/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java +++ b/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java @@ -16,16 +16,24 @@ package git4idea.annotate; import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsKey; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.annotate.LineAnnotationAspect; import com.intellij.openapi.vcs.annotate.LineAnnotationAspectAdapter; import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction; +import com.intellij.openapi.vcs.changes.ContentRevision; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.hash.HashMap; import com.intellij.util.text.DateFormatUtil; +import com.intellij.vcs.log.VcsUser; +import com.intellij.vcsUtil.VcsUtil; +import git4idea.GitContentRevision; import git4idea.GitFileRevision; import git4idea.GitRevisionNumber; import git4idea.GitVcs; @@ -33,8 +41,7 @@ import git4idea.i18n.GitBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Date; -import java.util.List; +import java.util.*; public class GitFileAnnotation extends FileAnnotation { private final Project myProject; @@ -42,49 +49,41 @@ public class GitFileAnnotation extends FileAnnotation { @NotNull private final GitVcs myVcs; @Nullable private final VcsRevisionNumber myBaseRevision; - @NotNull private final String myAnnotatedContent; @NotNull private final List myLines; - @NotNull private final List myRevisions; + @Nullable private List myRevisions; private final LineAnnotationAspect DATE_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.DATE, true) { @Override public String doGetValue(LineInfo info) { - final Date date = info.getDate(); - return date == null ? "" : DateFormatUtil.formatPrettyDate(date); + return DateFormatUtil.formatPrettyDate(info.getDate()); } }; private final LineAnnotationAspect REVISION_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.REVISION, false) { @Override protected String doGetValue(LineInfo lineInfo) { - final GitRevisionNumber revision = lineInfo.getRevision(); - return revision == null ? "" : String.valueOf(revision.getShortRev()); + return String.valueOf(lineInfo.getRevisionNumber().getShortRev()); } }; private final LineAnnotationAspect AUTHOR_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.AUTHOR, true) { @Override protected String doGetValue(LineInfo lineInfo) { - final String author = lineInfo.getAuthor(); - return author == null ? "" : author; + return lineInfo.getAuthor(); } }; public GitFileAnnotation(@NotNull final Project project, @NotNull VirtualFile file, @Nullable final VcsRevisionNumber revision, - @NotNull String annotatedContent, - @NotNull List lines, - @NotNull List revisions) { + @NotNull List lines) { super(project); myProject = project; myFile = file; myVcs = ObjectUtils.assertNotNull(GitVcs.getInstance(myProject)); myBaseRevision = revision == null ? (myVcs.getDiffProvider().getCurrentRevision(file)) : revision; - myAnnotatedContent = annotatedContent; myLines = lines; - myRevisions = revisions; } @Override @@ -96,25 +95,16 @@ public class GitFileAnnotation extends FileAnnotation { return new LineAnnotationAspect[]{REVISION_ASPECT, DATE_ASPECT, AUTHOR_ASPECT}; } - @Override - public String getToolTip(final int lineNumber) { - if (myLines.size() <= lineNumber || lineNumber < 0) { - return ""; - } - final LineInfo info = myLines.get(lineNumber); - VcsFileRevision fileRevision = info.myFileRevision; - if (fileRevision != null) { - return GitBundle.message("annotation.tool.tip", info.getRevision().asString(), info.getAuthor(), - DateFormatUtil.formatDateTime(info.getDate()), fileRevision.getCommitMessage()); - } - else { - return ""; - } - } - + @Nullable @Override public String getAnnotatedContent() { - return myAnnotatedContent; + try { + ContentRevision revision = GitContentRevision.createRevision(myFile, myBaseRevision, myProject); + return revision.getContent(); + } + catch (VcsException e) { + return null; + } } @Override @@ -122,33 +112,49 @@ public class GitFileAnnotation extends FileAnnotation { return myRevisions; } + public void setRevisions(@NotNull List revisions) { + myRevisions = revisions; + } + @Override public int getLineCount() { return myLines.size(); } + @Nullable + public LineInfo getLineInfo(int lineNumber) { + if (lineNumberCheck(lineNumber)) return null; + return myLines.get(lineNumber); + } + + @Nullable @Override - public VcsRevisionNumber getLineRevisionNumber(final int lineNumber) { - if (lineNumberCheck(lineNumber)) { - return null; - } - return myLines.get(lineNumber).getRevision(); + public String getToolTip(int lineNumber) { + LineInfo lineInfo = getLineInfo(lineNumber); + return lineInfo != null ? lineInfo.getTooltip() : null; + } + + @Nullable + @Override + public VcsRevisionNumber getLineRevisionNumber(int lineNumber) { + LineInfo lineInfo = getLineInfo(lineNumber); + return lineInfo != null ? lineInfo.getRevisionNumber() : null; + } + + @Nullable + @Override + public Date getLineDate(int lineNumber) { + LineInfo lineInfo = getLineInfo(lineNumber); + return lineInfo != null ? lineInfo.getDate() : null; } private boolean lineNumberCheck(int lineNumber) { return myLines.size() <= lineNumber || lineNumber < 0; } - @Override - public Date getLineDate(int lineNumber) { - if (lineNumberCheck(lineNumber)) { - return null; - } - return myLines.get(lineNumber).getDate(); - } - - public int getNumLines() { - return myLines.size(); + @NotNull + public List getLines() { + return myLines; } /** @@ -175,37 +181,74 @@ public class GitFileAnnotation extends FileAnnotation { protected void showAffectedPaths(int lineNum) { if (lineNum >= 0 && lineNum < myLines.size()) { LineInfo info = myLines.get(lineNum); - ShowAllAffectedGenericAction.showSubmittedFiles(myProject, info.getRevision(), myFile, GitVcs.getKey()); + ShowAllAffectedGenericAction.showSubmittedFiles(myProject, info.getRevisionNumber(), myFile, GitVcs.getKey()); } } } - /** - * Line information - */ static class LineInfo { - private final Date myDate; - private final GitRevisionNumber myRevision; - private final GitFileRevision myFileRevision; - private final String myAuthor; + @NotNull private final Project myProject; + @NotNull private final GitRevisionNumber myRevision; + @NotNull private final FilePath myFilePath; + @Nullable private final GitRevisionNumber myPreviousRevision; + @Nullable private final FilePath myPreviousFilePath; + @NotNull private final Date myDate; + @NotNull private final VcsUser myAuthor; + @NotNull private final String mySubject; - public LineInfo(Date date, GitRevisionNumber revision, GitFileRevision fileRevision, String author) { - myDate = date; + public LineInfo(@NotNull Project project, + @NotNull GitRevisionNumber revision, + @NotNull FilePath path, + @NotNull Date date, + @NotNull VcsUser author, + @NotNull String subject, + @Nullable GitRevisionNumber previousRevision, + @Nullable FilePath previousPath) { + myProject = project; myRevision = revision; - myFileRevision = fileRevision; + myFilePath = path; + myPreviousRevision = previousRevision; + myPreviousFilePath = previousPath; + myDate = date; myAuthor = author; + mySubject = subject; } + @NotNull + public GitRevisionNumber getRevisionNumber() { + return myRevision; + } + + @NotNull + public FilePath getFilePath() { + return myFilePath; + } + + @NotNull + public VcsFileRevision getFileRevision() { + return new GitFileRevision(myProject, myFilePath, myRevision); + } + + @Nullable + public VcsFileRevision getPreviousFileRevision() { + if (myPreviousRevision == null || myPreviousFilePath == null) return null; + return new GitFileRevision(myProject, myPreviousFilePath, myPreviousRevision); + } + + @NotNull public Date getDate() { return myDate; } - public GitRevisionNumber getRevision() { - return myRevision; + @NotNull + public String getAuthor() { + return myAuthor.getName(); } - public String getAuthor() { - return myAuthor; + @NotNull + public String getTooltip() { + return GitBundle.message("annotation.tool.tip", myRevision.asString(), myAuthor.getName(), + DateFormatUtil.formatDateTime(myDate), mySubject); } } @@ -231,4 +274,79 @@ public class GitFileAnnotation extends FileAnnotation { final VcsRevisionNumber currentCurrentRevision = myVcs.getDiffProvider().getCurrentRevision(myFile); return myBaseRevision != null && ! myBaseRevision.equals(currentCurrentRevision); } + + + @Nullable + @Override + public CurrentFileRevisionProvider getCurrentFileRevisionProvider() { + return (lineNumber) -> { + LineInfo lineInfo = getLineInfo(lineNumber); + return lineInfo != null ? lineInfo.getFileRevision() : null; + }; + } + + @Nullable + @Override + public PreviousFileRevisionProvider getPreviousFileRevisionProvider() { + return new PreviousFileRevisionProvider() { + @Nullable + @Override + public VcsFileRevision getPreviousRevision(int lineNumber) { + LineInfo lineInfo = getLineInfo(lineNumber); + return lineInfo != null ? lineInfo.getPreviousFileRevision() : null; + } + + @Nullable + @Override + public VcsFileRevision getLastRevision() { + if (myBaseRevision instanceof GitRevisionNumber) { + return new GitFileRevision(myProject, VcsUtil.getFilePath(myFile), (GitRevisionNumber)myBaseRevision); + } + else { + return ContainerUtil.getFirstItem(getRevisions()); + } + } + }; + } + + @Nullable + @Override + public AuthorsMappingProvider getAuthorsMappingProvider() { + Map authorsMap = new HashMap<>(); + for (int i = 0; i < getLineCount(); i++) { + LineInfo lineInfo = getLineInfo(i); + if (lineInfo == null) continue; + + if (!authorsMap.containsKey(lineInfo.getRevisionNumber())) { + authorsMap.put(lineInfo.getRevisionNumber(), lineInfo.getAuthor()); + } + } + + return () -> authorsMap; + } + + @Nullable + @Override + public RevisionsOrderProvider getRevisionsOrderProvider() { + ContainerUtil.KeyOrderedMultiMap dates = new ContainerUtil.KeyOrderedMultiMap<>(); + + for (int i = 0; i < getLineCount(); i++) { + LineInfo lineInfo = getLineInfo(i); + if (lineInfo == null) continue; + + VcsRevisionNumber number = lineInfo.getRevisionNumber(); + Date date = lineInfo.getDate(); + + dates.putValue(date, number); + } + + List> orderedRevisions = new ArrayList<>(); + NavigableSet orderedDates = dates.navigableKeySet(); + for (Date date : orderedDates.descendingSet()) { + Collection revisionNumbers = dates.get(date); + orderedRevisions.add(new ArrayList<>(revisionNumbers)); + } + + return () -> orderedRevisions; + } } diff --git a/plugins/git4idea/src/git4idea/history/GitHistoryProvider.java b/plugins/git4idea/src/git4idea/history/GitHistoryProvider.java index f854fae1861e..9002ecd849ac 100644 --- a/plugins/git4idea/src/git4idea/history/GitHistoryProvider.java +++ b/plugins/git4idea/src/git4idea/history/GitHistoryProvider.java @@ -102,7 +102,7 @@ public class GitHistoryProvider implements VcsHistoryProviderEx, } @Nullable - public VcsHistorySession createSessionFor(final FilePath filePath) throws VcsException { + public VcsAbstractHistorySession createSessionFor(final FilePath filePath) throws VcsException { List revisions = null; try { revisions = GitHistoryUtils.history(myProject, filePath);