[vcs-log] commit message prefix navigation in VCS Log table (IJPL-89240)

GitOrigin-RevId: 3b0196284ac76797ee92b7f050643e01393850a5
This commit is contained in:
Dmitry Zhuravlev
2024-05-28 14:50:15 +02:00
committed by intellij-monorepo-bot
parent 6100fb3643
commit d641d05793
23 changed files with 794 additions and 92 deletions

View File

@@ -847,6 +847,8 @@
<history.activityPresentationProvider implementation="git4idea.GitActivityPresentationProvider"/>
<searchScopesProvider implementation="git4idea.search.GitSearchScopeProvider"/>
<projectService serviceInterface="com.intellij.vcs.log.ui.table.links.CommitLinksProvider"
serviceImplementation="git4idea.log.GitCommitLinkProvider"/>
</extensions>
<extensions defaultExtensionNs="Git4Idea">

View File

@@ -0,0 +1,205 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.log
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vcs.LinkDescriptor
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.update.MergingUpdateQueue
import com.intellij.util.ui.update.Update
import com.intellij.vcs.log.CommitId
import com.intellij.vcs.log.VcsCommitMetadata
import com.intellij.vcs.log.data.VcsLogData
import com.intellij.vcs.log.graph.impl.facade.VisibleGraphImpl
import com.intellij.vcs.log.graph.utils.DfsWalk
import com.intellij.vcs.log.graph.utils.LinearGraphUtils
import com.intellij.vcs.log.graph.utils.isAncestor
import com.intellij.vcs.log.ui.table.links.CommitLinksProvider
import com.intellij.vcs.log.ui.table.links.CommitLinksResolveListener
import com.intellij.vcs.log.ui.table.links.NavigateToCommit
import com.intellij.vcs.log.visible.VisiblePack
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
internal class GitCommitLinkProvider(private val project: Project) : CommitLinksProvider {
override fun getLinks(commitId: CommitId): List<LinkDescriptor> {
return project.service<GitLinkToCommitResolver>().getLinks(commitId)
}
override fun resolveLinks(logId: String, logData: VcsLogData, visiblePack: VisiblePack,
startRow: Int, endRow: Int) {
project.service<GitLinkToCommitResolver>().submitResolveLinks(logId, logData, visiblePack,
startRow, endRow)
}
}
@Service(Service.Level.PROJECT)
internal class GitLinkToCommitResolver(private val project: Project) {
companion object {
private const val PREFIX_DELIMITER_LENGTH = 1
private val prefixes = listOf("fixup!", "squash!", "amend!")
private val regex = "^(${prefixes.joinToString("|")}) (.*)$".toRegex()
private const val CACHE_MAX_SIZE = 1_000L
}
private val prefixesCache: Cache<CommitId, List<PrefixTarget>> =
Caffeine.newBuilder()
.maximumSize(CACHE_MAX_SIZE)
.build()
private val resolveQueue = MergingUpdateQueue("resolve links queue", 100, true, null, project, null, false)
private val updateQueue = MergingUpdateQueue("after resolve links ui update queue", 100, true, null, project, null, true)
internal fun getLinks(commitId: CommitId): List<LinkDescriptor> {
return getCachedOrEmpty(commitId).map { NavigateToCommit(it.range, it.targetHash) }
}
internal fun submitResolveLinks(
logId: String,
logData: VcsLogData,
visiblePack: VisiblePack,
startRow: Int,
endRow: Int
) {
if (visiblePack.visibleGraph !is VisibleGraphImpl) return
val startFrom = max(0, startRow)
val end = max(0, endRow)
val rowRange = startFrom..end
val processingCount = min(abs(Registry.intValue("vcs.log.render.commit.links.process.chunk")), rowRange.count())
if (processingCount < 2) return
val visibleGraph = visiblePack.visibleGraph
resolveQueue.queue(Update.create(logId + startFrom) {
for (i in rowRange) {
val commitId = visibleGraph.getRowInfo(i).commit
val commit = logData.commitMetadataCache.getCachedData(commitId) ?: continue
resolveLinks(logData, visiblePack, commit.getCommitId(), commit.subject, processingCount)
}
updateQueue.queue(Update.create(logId) {
project.messageBus.syncPublisher(CommitLinksResolveListener.TOPIC).onLinksResolved(logId)
})
})
}
@RequiresBackgroundThread
internal fun resolveLinks(
logData: VcsLogData,
visiblePack: VisiblePack,
commitId: CommitId, commitMessage: String,
processingCount: Int
) {
if (visiblePack.visibleGraph !is VisibleGraphImpl) return
val cachedPrefixes = getCachedOrEmpty(commitId)
if (cachedPrefixes.isNotEmpty()) {
return
}
var match = regex.matchEntire(commitMessage) ?: return
var prefix = match.groups[1]?.value.orEmpty()
var rest = match.groups[2]?.value.orEmpty()
if (prefix.isBlank() && rest.isBlank()) return
var prefixOffset = 0
val existingPrefixes = cachedPrefixes.toMutableList()
while (prefix.isNotBlank() && rest.isNotBlank()) {
if (prefix.isNotBlank()) {
val prefixRange = TextRange.from(prefixOffset, prefix.length)
val targetHash = resolveHash(logData, visiblePack, commitId, rest, processingCount)
if (targetHash != null) {
existingPrefixes.add(PrefixTarget(prefixRange, targetHash))
}
prefixOffset += prefix.length + PREFIX_DELIMITER_LENGTH
}
match = regex.matchEntire(rest) ?: break
prefix = match.groups[1]?.value.orEmpty()
rest = match.groups[2]?.value.orEmpty()
}
if (existingPrefixes.isNotEmpty()) {
prefixesCache.put(commitId, existingPrefixes)
}
}
private fun resolveHash(
logData: VcsLogData,
visiblePack: VisiblePack,
commitId: CommitId, commitMessage: String,
processingCount: Int
): String? {
val sourceCommitId = logData.getCommitIndex(commitId.hash, commitId.root)
val visibleGraph = visiblePack.visibleGraph as VisibleGraphImpl
val sourceCommitNodeIndex = visibleGraph.getVisibleRowIndex(sourceCommitId) ?: return null
val liteLinearGraph = LinearGraphUtils.asLiteLinearGraph(visibleGraph.linearGraph)
var foundData: VcsCommitMetadata? = null
iterateCommits(logData, visiblePack, sourceCommitNodeIndex, processingCount) { currentData ->
val currentNodeId = visibleGraph.getVisibleRowIndex(currentData.getCommitIndex(logData))
if (currentNodeId != null && currentData.subject == commitMessage
&& liteLinearGraph.isAncestor(currentNodeId, sourceCommitNodeIndex)
) {
foundData = currentData
}
foundData != null
}
return foundData?.id?.toString()
}
private fun iterateCommits(
logData: VcsLogData, visiblePack: VisiblePack,
startFromCommitIndex: Int,
commitsCount: Int,
consumer: (VcsCommitMetadata) -> Boolean
) {
val visibleGraph = visiblePack.visibleGraph as VisibleGraphImpl
val linearGraph = visibleGraph.linearGraph
val walkDepth = min(commitsCount, visibleGraph.visibleCommitCount)
var visitedNode = 0
DfsWalk(listOf(startFromCommitIndex), linearGraph).walk(true) { currentNodeId ->
val currentCommitIndex = visibleGraph.getRowInfo(currentNodeId).commit
val currentCommitData = logData.commitMetadataCache.getCachedData(currentCommitIndex)
var consumed = false
visitedNode++
if (visitedNode > walkDepth) return@walk false
if (currentCommitData != null) {
if (currentCommitData.parents.size > 1) return@walk false
consumed = consumer(currentCommitData)
}
!consumed
}
}
private fun getCachedOrEmpty(commitId: CommitId): List<PrefixTarget> {
return prefixesCache.getIfPresent(commitId) ?: emptyList()
}
private fun VcsCommitMetadata.getCommitIndex(logData: VcsLogData): Int = logData.getCommitIndex(id, root)
private fun VcsCommitMetadata.getCommitId(): CommitId = CommitId(id, root)
private data class PrefixTarget(val range: TextRange, val targetHash: String)
}

View File

@@ -0,0 +1,128 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.log
import com.intellij.openapi.components.service
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vcs.LinkDescriptor
import com.intellij.vcs.log.CommitId
import com.intellij.vcs.log.data.VcsLogData
import com.intellij.vcs.log.data.VcsLogRefresherTest.LogRefresherTestHelper
import com.intellij.vcs.log.graph.PermanentGraph
import com.intellij.vcs.log.impl.HashImpl
import com.intellij.vcs.log.impl.VcsProjectLog
import com.intellij.vcs.log.ui.table.links.NavigateToCommit
import com.intellij.vcs.log.visible.VisiblePack
import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
import git4idea.test.GitSingleRepoTest
import git4idea.test.commit
class GitLinkToCommitResolverTest : GitSingleRepoTest() {
private lateinit var logData: VcsLogData
private lateinit var logRefresherHelper: LogRefresherTestHelper
private lateinit var visiblePack: VisiblePack
override fun setUp() {
super.setUp()
if (VcsProjectLog.ensureLogCreated(project)) {
val projectLog = VcsProjectLog.getInstance(project)
logData = projectLog.dataManager!!
logRefresherHelper = LogRefresherTestHelper(logData, VcsLogData.getRecentCommitsCount())
}
}
override fun tearDown() {
try {
logRefresherHelper.tearDown()
}
catch (e: Throwable) {
addSuppressedException(e)
}
finally {
super.tearDown()
}
}
fun `test resolve single fixup`() {
val fixupCommitMsg = "fixup! [subsystem] add file 1"
val commitMsg = "[subsystem] add file 1"
file("1.txt").create("File 1 content").add()
val commitHash = repo.commit(commitMsg)
file("2.txt").create("File 2 content").add()
val fixupCommitHash = HashImpl.build(repo.commit(fixupCommitMsg))
refreshVisibleGraph()
val resolver = project.service<GitLinkToCommitResolver>()
resolver.resolveLinks(CommitId(fixupCommitHash, repo.root), fixupCommitMsg)
val links = resolver.getLinks(CommitId(fixupCommitHash, repo.root))
assertTrue(links.size == 1)
assertEquals(links[0].target, commitHash)
assertEquals(links[0].range, TextRange.from(0, "fixup!".length))
assertEquals(links[0].range.substring(fixupCommitMsg), "fixup!")
}
fun `test resolve multiple prefixes`() {
val squashCommitMsg = "fixup! squash! add file 1"
val fixup2CommitMsg = "fixup! fixup! add file 1"
val fixup1CommitMsg = "fixup! add file 1"
val commitMsg = "add file 1"
file("1.txt").create("File 1 content").add()
val commitHash = repo.commit(commitMsg)
file("2.txt").create("File 2 content").add()
val fixup1CommitHash = HashImpl.build(repo.commit(fixup1CommitMsg))
file("3.txt").create("File 3 content").add()
val fixup2CommitHash = HashImpl.build(repo.commit(fixup2CommitMsg))
file("4.txt").create("File 4 content").add()
val squashCommitHash = HashImpl.build(repo.commit(squashCommitMsg))
refreshVisibleGraph()
val resolver = project.service<GitLinkToCommitResolver>()
resolver.resolveLinks(CommitId(fixup1CommitHash, repo.root), fixup1CommitMsg)
resolver.resolveLinks(CommitId(fixup2CommitHash, repo.root), fixup2CommitMsg)
resolver.resolveLinks(CommitId(squashCommitHash, repo.root), squashCommitMsg)
var links = resolver.getLinks(CommitId(fixup1CommitHash, repo.root))
assertTrue(links.size == 1)
assertEquals(links[0].target, commitHash)
assertEquals(links[0].range, TextRange.from(0, "fixup!".length))
assertEquals(links[0].range.substring(fixup1CommitMsg), "fixup!")
links = resolver.getLinks(CommitId(fixup2CommitHash, repo.root))
assertTrue(links.size == 2)
assertEquals(links[0].target, fixup1CommitHash.toString())
assertEquals(links[0].range, TextRange.from(0, "fixup!".length))
assertEquals(links[0].range.substring(fixup2CommitMsg), "fixup!")
assertEquals(links[1].target, commitHash)
assertEquals(links[1].range, TextRange.from(7, "fixup!".length))
assertEquals(links[1].range.substring(fixup2CommitMsg), "fixup!")
links = resolver.getLinks(CommitId(squashCommitHash, repo.root))
assertTrue(links.size == 1)
assertEquals(links[0].target, commitHash)
assertEquals(links[0].range, TextRange.from(7, "squash!".length))
assertEquals(links[0].range.substring(squashCommitMsg), "squash!")
}
private fun GitLinkToCommitResolver.resolveLinks(commitId: CommitId, commitMessage: @NlsSafe String) {
resolveLinks(logData, visiblePack, commitId, commitMessage, Registry.intValue("vcs.log.render.commit.links.process.chunk"))
}
private fun refreshVisibleGraph() {
logRefresherHelper.initAndWaitForFirstRefresh()
val dataPack = logRefresherHelper.dataPack
val visibleGraph = dataPack.permanentGraph.createVisibleGraph(PermanentGraph.Options.Default, null, null)
visiblePack = VisiblePack(dataPack, visibleGraph, false, VcsLogFilterObject.collection())
}
private val LinkDescriptor.target get() = (this as NavigateToCommit).target
}