[vcs-log] implement "Show in File History" action for the annotations context menu

GitOrigin-RevId: 8d10e772c0270b444e67a23bae747a40150ba2d3
This commit is contained in:
Julia Beliaeva
2024-03-28 19:24:40 +00:00
committed by intellij-monorepo-bot
parent 15fd756008
commit 0fca4e09bd
6 changed files with 196 additions and 121 deletions
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2015 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.
*/
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.actions;
import com.intellij.openapi.actionSystem.AnActionEvent;
@@ -25,22 +11,15 @@ import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.annotate.FileAnnotation;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.TextRevisionNumber;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsFileRevisionEx;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.impl.BackgroundableActionLock;
import com.intellij.openapi.vcs.vfs.ContentRevisionVirtualFile;
import com.intellij.openapi.vcs.vfs.VcsVirtualFile;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.vcs.AnnotationProviderEx;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Objects;
@@ -60,13 +39,13 @@ public class AnnotateVcsVirtualFileAction {
if (VcsAnnotateUtil.getEditorFor(file, e.getDataContext()) == null) return false;
AnnotationData data = extractData(project, file);
AnnotationData data = AnnotationData.extractFrom(project, file);
if (data == null) return false;
AnnotationProviderEx provider = ObjectUtils.tryCast(data.vcs.getAnnotationProvider(), AnnotationProviderEx.class);
AnnotationProviderEx provider = ObjectUtils.tryCast(data.getVcs().getAnnotationProvider(), AnnotationProviderEx.class);
if (provider == null) return false;
return provider.isAnnotationValid(data.filePath, data.revisionNumber);
return provider.isAnnotationValid(data.getFilePath(), data.getRevisionNumber());
}
private static boolean isSuspended(@NotNull AnActionEvent e) {
@@ -94,10 +73,10 @@ public class AnnotateVcsVirtualFileAction {
}
private static void doAnnotate(@NotNull final Project project, @NotNull final Editor editor, @NotNull final VirtualFile file) {
final AnnotationData data = extractData(project, file);
final AnnotationData data = AnnotationData.extractFrom(project, file);
assert data != null;
final AnnotationProviderEx provider = (AnnotationProviderEx)data.vcs.getAnnotationProvider();
final AnnotationProviderEx provider = (AnnotationProviderEx)data.getVcs().getAnnotationProvider();
assert provider != null;
@@ -111,7 +90,7 @@ public class AnnotateVcsVirtualFileAction {
@Override
public void run(final @NotNull ProgressIndicator indicator) {
try {
fileAnnotationRef.set(provider.annotate(data.filePath, data.revisionNumber));
fileAnnotationRef.set(provider.annotate(data.getFilePath(), data.getRevisionNumber()));
}
catch (VcsException e) {
exceptionRef.set(e);
@@ -138,7 +117,7 @@ public class AnnotateVcsVirtualFileAction {
}
if (!fileAnnotationRef.isNull()) {
AnnotateToggleAction.doAnnotate(editor, project, fileAnnotationRef.get(), data.vcs);
AnnotateToggleAction.doAnnotate(editor, project, fileAnnotationRef.get(), data.getVcs());
}
}
@@ -150,49 +129,6 @@ public class AnnotateVcsVirtualFileAction {
ProgressManager.getInstance().run(annotateTask);
}
@Nullable
private static AnnotationData extractData(@NotNull Project project, @NotNull VirtualFile file) {
FilePath filePath = null;
VcsRevisionNumber revisionNumber = null;
if (file instanceof VcsVirtualFile) {
VcsFileRevision revision = ((VcsVirtualFile)file).getFileRevision();
if (revision instanceof VcsFileRevisionEx) {
filePath = ((VcsFileRevisionEx)revision).getPath();
}
else {
filePath = VcsUtil.getFilePath(file);
}
revisionNumber = revision != null ? revision.getRevisionNumber() : null;
}
else if (file instanceof ContentRevisionVirtualFile) {
ContentRevision revision = ((ContentRevisionVirtualFile)file).getContentRevision();
filePath = revision.getFile();
revisionNumber = revision.getRevisionNumber();
}
if (filePath == null || revisionNumber == null) return null;
if (revisionNumber instanceof TextRevisionNumber ||
revisionNumber == VcsRevisionNumber.NULL) {
return null;
}
AbstractVcs vcs = VcsUtil.getVcsFor(project, filePath);
return vcs != null ? new AnnotationData(vcs, filePath, revisionNumber) : null;
}
private static class AnnotationData {
@NotNull public final AbstractVcs vcs;
@NotNull public final FilePath filePath;
@NotNull public final VcsRevisionNumber revisionNumber;
AnnotationData(@NotNull AbstractVcs vcs,
@NotNull FilePath filePath,
@NotNull VcsRevisionNumber revisionNumber) {
this.vcs = vcs;
this.filePath = filePath;
this.revisionNumber = revisionNumber;
}
}
public static class Provider implements AnnotateToggleAction.Provider {
@Override
public boolean isEnabled(AnActionEvent e) {
@@ -0,0 +1,41 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.actions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.AbstractVcs
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.changes.TextRevisionNumber
import com.intellij.openapi.vcs.history.VcsFileRevisionEx
import com.intellij.openapi.vcs.history.VcsRevisionNumber
import com.intellij.openapi.vcs.vfs.ContentRevisionVirtualFile
import com.intellij.openapi.vcs.vfs.VcsVirtualFile
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.vcsUtil.VcsUtil
data class AnnotationData(val vcs: AbstractVcs, val filePath: FilePath, val revisionNumber: VcsRevisionNumber) {
companion object {
@JvmStatic
fun extractFrom(project: Project, file: VirtualFile): AnnotationData? {
val filePath: FilePath
val revisionNumber: VcsRevisionNumber
when (file) {
is VcsVirtualFile -> {
val revision = file.fileRevision ?: return null
filePath = if (revision is VcsFileRevisionEx) revision.path else VcsUtil.getFilePath(file)
revisionNumber = revision.revisionNumber
}
is ContentRevisionVirtualFile -> {
val revision = file.contentRevision
filePath = revision.file
revisionNumber = revision.revisionNumber
}
else -> return null
}
if (revisionNumber is TextRevisionNumber || revisionNumber === VcsRevisionNumber.NULL) return null
val vcs = VcsUtil.getVcsFor(project, filePath) ?: return null
return AnnotationData(vcs, filePath, revisionNumber)
}
}
}
@@ -93,6 +93,7 @@ action.Vcs.Log.ResumeIndexing.text=Resume Indexing
action.Vcs.Log.ResumeIndexing.description=Indexing was paused as it took longer than expected. Resume.
action.Vcs.Log.SelectInLog.text=Show in VCS Log
action.Vcs.Log.SelectInLog.text.template=Show in {0} Log
vcs.log.action.show.in.file.history.text=Show in File History
group.Vcs.Log.PresentationSettings.text=View Options
group.Vcs.Log.PresentationSettings.description=Configure presentation of the Log
group.Vcs.Log.ChangesBrowser.PresentationSettings.text=View Options
@@ -164,6 +165,7 @@ file.history.commit.not.found.in.branch={0} does not exist in history for {1} in
file.history.commit.not.found={0} does not exist in history for {1}
file.history.commit.not.found.view.and.show.all.branches.link=View and show all branches
file.history.commit.not.found.view.in.log.link=View in Log
file.history.show.commit.in.history.process=Searching commit {0} in file history
# indexing progress
vcs.log.status.bar.indexing.cancel.tooltip=Cancel indexing. Indexing could be restarted manually from {0} Log toolbar.
@@ -81,6 +81,7 @@
<openapi.vcs.history.actions.GetVersionAction.ExtensionProvider
implementation="com.intellij.vcs.log.ui.actions.history.GetVersionFromHistoryAction"/>
<openapi.vcs.changes.actions.diff.ChangeDiffRequestProvider implementation="com.intellij.vcs.log.impl.MergedChangeDiffRequestProvider"/>
<vcsAnnotationGutterActionProvider implementation="com.intellij.vcs.log.ui.actions.history.ShowInFileHistoryAnnotationActionProvider" />
<actionPromoter implementation="com.intellij.vcs.log.ui.actions.VcsLogActionPromoter"/>
@@ -21,25 +21,43 @@ import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
import com.intellij.vcs.log.visible.filters.matches
import com.intellij.vcsUtil.VcsUtil
class VcsLogFileHistoryProviderImpl(project: Project) : VcsLogFileHistoryProvider {
private val providers = listOf(VcsLogSingleFileHistoryProvider(project), VcsLogDirectoryHistoryProvider(project))
class VcsLogFileHistoryProviderImpl(private val project: Project) : VcsLogFileHistoryProvider {
override fun canShowFileHistory(paths: Collection<FilePath>, revisionNumber: String?): Boolean {
return providers.any { it.canShowFileHistory(paths, revisionNumber) }
return canShowFileHistory(project, paths, revisionNumber)
}
override fun showFileHistory(paths: Collection<FilePath>, revisionNumber: String?) {
providers.firstOrNull { it.canShowFileHistory(paths, revisionNumber) }?.showFileHistory(paths, revisionNumber)
selectProvider(project, paths, revisionNumber)?.showFileHistoryUi(project, paths, revisionNumber)
}
}
class VcsLogDirectoryHistoryProvider(private val project: Project) : VcsLogFileHistoryProvider {
override fun canShowFileHistory(paths: Collection<FilePath>, revisionNumber: String?): Boolean {
internal fun canShowFileHistory(project: Project, paths: Collection<FilePath>, revisionNumber: String?): Boolean {
return selectProvider(project, paths, revisionNumber) != null
}
internal fun showFileHistoryUi(project: Project, paths: Collection<FilePath>, revisionNumber: String?, selectRow: Boolean = false): VcsLogUiEx? {
return selectProvider(project, paths, revisionNumber)?.showFileHistoryUi(project, paths, revisionNumber, selectRow)
}
private fun selectProvider(project: Project, paths: Collection<FilePath>, revisionNumber: String?): FileHistoryLogUiProvider? {
return listOf(VcsLogSingleFileHistoryProvider, VcsLogDirectoryHistoryProvider).firstOrNull {
it.canShowFileHistory(project, paths, revisionNumber)
}
}
interface FileHistoryLogUiProvider {
fun canShowFileHistory(project: Project, paths: Collection<FilePath>, revisionNumber: String?): Boolean
fun showFileHistoryUi(project: Project, paths: Collection<FilePath>, revisionNumber: String?, selectRow: Boolean = true): VcsLogUiEx?
}
object VcsLogDirectoryHistoryProvider : FileHistoryLogUiProvider {
override fun canShowFileHistory(project: Project, paths: Collection<FilePath>, revisionNumber: String?): Boolean {
val dataManager = VcsProjectLog.getInstance(project).dataManager ?: return false
return createPathsFilter(project, dataManager.logProviders, paths) != null
}
override fun showFileHistory(paths: Collection<FilePath>, revisionNumber: String?) {
override fun showFileHistoryUi(project: Project, paths: Collection<FilePath>, revisionNumber: String?, selectRow: Boolean): VcsLogUiEx? {
val hash = revisionNumber?.let { HashImpl.build(it) }
val root = VcsLogUtil.getActualRoot(project, paths.first())!!
@@ -55,58 +73,58 @@ class VcsLogDirectoryHistoryProvider(private val project: Project) : VcsLogFileH
val firstTime = ui == null
if (firstTime) {
val filters = VcsLogFilterObject.collection(pathsFilter, hashFilter)
ui = VcsProjectLog.getInstance(project).openLogTab(filters) ?: return
ui = VcsProjectLog.getInstance(project).openLogTab(filters) ?: return null
ui.properties[MainVcsLogUiProperties.SHOW_ONLY_AFFECTED_CHANGES] = true
}
selectRowWhenOpen(logManager, hash, root, ui!!, firstTime)
if (selectRow) selectRowWhenOpen(logManager, hash, root, ui!!, firstTime)
return ui
}
companion object {
fun createPathsFilter(project: Project, logProviders: Map<VirtualFile, VcsLogProvider>, paths: Collection<FilePath>): VcsLogFilter? {
val forRootFilter = mutableSetOf<VirtualFile>()
val forPathsFilter = mutableListOf<FilePath>()
for (path in paths) {
val root = VcsLogUtil.getActualRoot(project, path)
if (root == null) return null
if (!logProviders.keys.contains(root) ||
!VcsLogProperties.SUPPORTS_LOG_DIRECTORY_HISTORY.getOrDefault(logProviders[root])) return null
fun createPathsFilter(project: Project, logProviders: Map<VirtualFile, VcsLogProvider>, paths: Collection<FilePath>): VcsLogFilter? {
val forRootFilter = mutableSetOf<VirtualFile>()
val forPathsFilter = mutableListOf<FilePath>()
for (path in paths) {
val root = VcsLogUtil.getActualRoot(project, path)
if (root == null) return null
if (!logProviders.keys.contains(root) ||
!VcsLogProperties.SUPPORTS_LOG_DIRECTORY_HISTORY.getOrDefault(logProviders[root])) return null
val correctedPath = getCorrectedPath(project, path, false)
if (!correctedPath.isDirectory) return null
val correctedPath = getCorrectedPath(project, path, false)
if (!correctedPath.isDirectory) return null
if (path.virtualFile == root) {
forRootFilter.add(root)
}
else {
forPathsFilter.add(correctedPath)
}
if (forPathsFilter.isNotEmpty() && forRootFilter.isNotEmpty()) return null
if (path.virtualFile == root) {
forRootFilter.add(root)
}
else {
forPathsFilter.add(correctedPath)
}
if (forPathsFilter.isNotEmpty()) return VcsLogFilterObject.fromPaths(forPathsFilter)
return VcsLogFilterObject.fromRoots(forRootFilter)
if (forPathsFilter.isNotEmpty() && forRootFilter.isNotEmpty()) return null
}
private fun createHashFilter(hash: Hash?, root: VirtualFile): VcsLogFilter {
if (hash == null) {
return VcsLogFilterObject.fromBranch(VcsLogUtil.HEAD)
}
if (forPathsFilter.isNotEmpty()) return VcsLogFilterObject.fromPaths(forPathsFilter)
return VcsLogFilterObject.fromRoots(forRootFilter)
}
return VcsLogFilterObject.fromCommit(CommitId(hash, root))
private fun createHashFilter(hash: Hash?, root: VirtualFile): VcsLogFilter {
if (hash == null) {
return VcsLogFilterObject.fromBranch(VcsLogUtil.HEAD)
}
private fun matches(filters: VcsLogFilterCollection, pathsFilter: VcsLogFilter, hashFilter: VcsLogFilter): Boolean {
if (!filters.matches(hashFilter.key, pathsFilter.key)) {
return false
}
return filters.get(pathsFilter.key) == pathsFilter && filters.get(hashFilter.key) == hashFilter
return VcsLogFilterObject.fromCommit(CommitId(hash, root))
}
private fun matches(filters: VcsLogFilterCollection, pathsFilter: VcsLogFilter, hashFilter: VcsLogFilter): Boolean {
if (!filters.matches(hashFilter.key, pathsFilter.key)) {
return false
}
return filters.get(pathsFilter.key) == pathsFilter && filters.get(hashFilter.key) == hashFilter
}
}
private class VcsLogSingleFileHistoryProvider(private val project: Project) : VcsLogFileHistoryProvider {
override fun canShowFileHistory(paths: Collection<FilePath>, revisionNumber: String?): Boolean {
private object VcsLogSingleFileHistoryProvider : FileHistoryLogUiProvider {
override fun canShowFileHistory(project: Project, paths: Collection<FilePath>, revisionNumber: String?): Boolean {
if (!isNewHistoryEnabled() || paths.size != 1) return false
val root = VcsLogUtil.getActualRoot(project, paths.single()) ?: return false
@@ -117,12 +135,12 @@ private class VcsLogSingleFileHistoryProvider(private val project: Project) : Vc
return logProvider?.diffHandler != null && logProvider.getFileHistoryHandler(project) != null
}
override fun showFileHistory(paths: Collection<FilePath>, revisionNumber: String?) {
if (paths.size != 1) return
override fun showFileHistoryUi(project: Project, paths: Collection<FilePath>, revisionNumber: String?, selectRow: Boolean): VcsLogUiEx? {
if (paths.size != 1) return null
val root = VcsLogUtil.getActualRoot(project, paths.first())!!
val path = getCorrectedPath(project, paths.single(), revisionNumber != null)
if (path.isDirectory) return
if (path.isDirectory) return null
val hash = revisionNumber?.let { HashImpl.build(it) }
triggerFileHistoryUsage(project, paths, hash)
@@ -136,7 +154,9 @@ private class VcsLogSingleFileHistoryProvider(private val project: Project) : Vc
if (firstTime) {
fileHistoryUi = project.service<FileHistoryTabsManager>().openFileHistoryTab(logManager, path, root, hash)
}
selectRowWhenOpen(logManager, hash, root, fileHistoryUi!!, firstTime)
if (selectRow) selectRowWhenOpen(logManager, hash, root, fileHistoryUi!!, firstTime)
return fileHistoryUi
}
}
@@ -0,0 +1,75 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.vcs.log.ui.actions.history
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.vcs.actions.AnnotationData
import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup
import com.intellij.openapi.vcs.annotate.AnnotationGutterActionProvider
import com.intellij.openapi.vcs.annotate.FileAnnotation
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.io.await
import com.intellij.vcs.log.VcsLogBundle
import com.intellij.vcs.log.history.canShowFileHistory
import com.intellij.vcs.log.history.showFileHistoryUi
import com.intellij.vcs.log.impl.VcsLogNavigationUtil.jumpToHash
import com.intellij.vcs.log.impl.VcsProjectLog
import com.intellij.vcs.log.util.VcsLogUtil
import com.intellij.vcsUtil.VcsUtil
import kotlinx.coroutines.launch
class ShowInFileHistoryAnnotationActionProvider : AnnotationGutterActionProvider {
override fun createAction(annotation: FileAnnotation): AnAction {
return ShowInFileHistoryAnnotationAction(annotation)
}
}
private class ShowInFileHistoryAnnotationAction(private val annotation: FileAnnotation) :
DumbAwareAction(VcsLogBundle.message("vcs.log.action.show.in.file.history.text")) {
override fun update(e: AnActionEvent) {
val project = e.project
val file = annotation.file
val lineRevisionNumber = getLineRevisionNumber(e)
if (project == null || file == null || lineRevisionNumber == null) {
e.presentation.isEnabledAndVisible = false
return
}
val annotationData = AnnotationData.extractFrom(project, file)
val annotatedFilePath = annotationData?.filePath ?: VcsUtil.getFilePath(file)
val annotatedRevisionNumber = annotationData?.revisionNumber?.asString()
e.presentation.isEnabledAndVisible = canShowFileHistory(project, listOf(annotatedFilePath), annotatedRevisionNumber)
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val file = annotation.file ?: return
val lineRevisionNumber = getLineRevisionNumber(e) ?: return
val annotationData = AnnotationData.extractFrom(project, file)
val annotatedFilePath = annotationData?.filePath ?: VcsUtil.getFilePath(file)
val annotatedRevisionNumber = annotationData?.revisionNumber?.asString()
val ui = showFileHistoryUi(project, listOf(annotatedFilePath), annotatedRevisionNumber) ?: return
val future = ui.jumpToHash(lineRevisionNumber, false, true)
VcsProjectLog.getInstance(project).childScope().launch {
withBackgroundProgress(project,
VcsLogBundle.message("file.history.show.commit.in.history.process",
VcsLogUtil.getShortHash(lineRevisionNumber)), true) {
future.await()
}
}
}
private fun getLineRevisionNumber(e: AnActionEvent): String? {
val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(e.dataContext)
return annotation.getLineRevisionNumber(lineNumber)?.asString()
}
override fun getActionUpdateThread() = ActionUpdateThread.BGT
}