mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 11:50:54 +07:00
[vcs-log] commit message prefix navigation in VCS Log table (IJPL-89240)
GitOrigin-RevId: 3b0196284ac76797ee92b7f050643e01393850a5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
6100fb3643
commit
d641d05793
@@ -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">
|
||||
|
||||
205
plugins/git4idea/src/git4idea/log/GitLinkToCommitResolver.kt
Normal file
205
plugins/git4idea/src/git4idea/log/GitLinkToCommitResolver.kt
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user