Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 882cc27

Browse files
committedJun 21, 2025··
chore: revert smart commit with Github issue id
1 parent 766eb22 commit 882cc27

File tree

1 file changed

+48
-395
lines changed

1 file changed

+48
-395
lines changed
 

‎exts/ext-git/src/main/kotlin/cc/unitmesh/git/actions/vcs/CommitMessageSuggestionAction.kt

Lines changed: 48 additions & 395 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cc.unitmesh.git.actions.vcs
22

33
import cc.unitmesh.devti.AutoDevNotifications
44
import cc.unitmesh.devti.actions.chat.base.ChatBaseAction
5-
import cc.unitmesh.devti.flow.kanban.impl.GitHubIssue
65
import cc.unitmesh.devti.gui.chat.message.ChatActionType
76
import cc.unitmesh.devti.llms.LlmFactory
87
import cc.unitmesh.devti.settings.locale.LanguageChangedCallback.presentationText
@@ -14,158 +13,57 @@ import cc.unitmesh.devti.template.context.TemplateContext
1413
import cc.unitmesh.devti.util.AutoDevCoroutineScope
1514
import cc.unitmesh.devti.util.parser.CodeFence
1615
import cc.unitmesh.devti.vcs.VcsUtil
17-
import com.intellij.ide.TextCopyProvider
1816
import com.intellij.openapi.actionSystem.ActionUpdateThread
1917
import com.intellij.openapi.actionSystem.AnActionEvent
20-
import com.intellij.openapi.actionSystem.PlatformDataKeys.COPY_PROVIDER
2118
import com.intellij.openapi.application.ApplicationManager
2219
import com.intellij.openapi.application.invokeLater
2320
import com.intellij.openapi.components.service
2421
import com.intellij.openapi.diagnostic.logger
25-
import com.intellij.openapi.progress.ProgressIndicator
26-
import com.intellij.openapi.progress.ProgressManager
27-
import com.intellij.openapi.progress.Task
2822
import com.intellij.openapi.project.Project
29-
import com.intellij.openapi.ui.Messages
30-
import com.intellij.openapi.ui.popup.JBPopup
31-
import com.intellij.openapi.ui.popup.JBPopupFactory
32-
import com.intellij.openapi.ui.popup.JBPopupListener
33-
import com.intellij.openapi.ui.popup.LightweightWindowEvent
3423
import com.intellij.openapi.vcs.VcsDataKeys
3524
import com.intellij.openapi.vcs.changes.Change
3625
import com.intellij.openapi.vcs.changes.CurrentContentRevision
3726
import com.intellij.openapi.vcs.ui.CommitMessage
3827
import com.intellij.openapi.vfs.VirtualFile
39-
import com.intellij.ui.ColoredListCellRenderer
40-
import com.intellij.ui.awt.RelativePoint
41-
import com.intellij.ui.speedSearch.SpeedSearchUtil.applySpeedSearchHighlighting
42-
import com.intellij.util.containers.nullize
43-
import com.intellij.util.ui.JBUI.scale
4428
import com.intellij.vcs.commit.CommitWorkflowUi
4529
import com.intellij.vcs.log.VcsLogFilterCollection
4630
import com.intellij.vcs.log.VcsLogProvider
4731
import com.intellij.vcs.log.impl.VcsProjectLog
4832
import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
4933
import kotlinx.coroutines.*
5034
import kotlinx.coroutines.flow.*
51-
import org.kohsuke.github.GHIssue
52-
import org.kohsuke.github.GHIssueState
53-
import java.awt.Point
54-
import java.util.concurrent.ConcurrentHashMap
55-
import javax.swing.JList
56-
import javax.swing.ListSelectionModel.SINGLE_SELECTION
57-
58-
data class IssueDisplayItem(val issue: GHIssue, val displayText: String)
59-
60-
/**
61-
* Cache entry for GitHub issues with timestamp for TTL management
62-
*/
63-
private data class IssueCacheEntry(
64-
val issues: List<IssueDisplayItem>,
65-
val timestamp: Long
66-
) {
67-
fun isExpired(ttlMs: Long): Boolean = System.currentTimeMillis() - timestamp > ttlMs
68-
}
6935

7036
class CommitMessageSuggestionAction : ChatBaseAction() {
7137
private val logger = logger<CommitMessageSuggestionAction>()
7238

7339
init {
7440
presentationText("settings.autodev.others.commitMessage", templatePresentation)
75-
isEnabledInModalContext = true
7641
}
7742

7843
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
7944

8045
private var currentJob: Job? = null
81-
private var selectedIssue: IssueDisplayItem? = null
82-
private var currentChanges: List<Change>? = null
83-
private var currentEvent: AnActionEvent? = null
84-
private var isGitHubRepository: Boolean = false
85-
86-
companion object {
87-
// In-memory cache for GitHub issues with 5-minute TTL
88-
private val issuesCache = ConcurrentHashMap<String, IssueCacheEntry>()
89-
private const val CACHE_TTL_MS = 5 * 60 * 1000L // 5 minutes
90-
private const val MAX_CACHE_SIZE = 50 // Maximum number of repositories to cache
91-
92-
// Cache statistics
93-
@Volatile
94-
private var cacheHits = 0
95-
96-
@Volatile
97-
private var cacheMisses = 0
98-
99-
/**
100-
* Clear all cached GitHub issues
101-
*/
102-
fun clearIssuesCache() {
103-
issuesCache.clear()
104-
logger<CommitMessageSuggestionAction>().info("GitHub issues cache cleared manually")
105-
}
106-
107-
/**
108-
* Get cache statistics for debugging
109-
*/
110-
fun getCacheStats(): String {
111-
val total = cacheHits + cacheMisses
112-
val hitRate = if (total > 0) (cacheHits * 100.0 / total) else 0.0
113-
return "Cache Stats - Hits: $cacheHits, Misses: $cacheMisses, Hit Rate: ${"%.2f".format(hitRate)}%, Entries: ${issuesCache.size}"
114-
}
115-
}
11646

11747
override fun getActionType(): ChatActionType = ChatActionType.GEN_COMMIT_MESSAGE
11848

11949
override fun update(e: AnActionEvent) {
120-
val project = e.project
12150
val data = e.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL)
122-
123-
if (data == null || project == null) {
51+
if (data == null) {
12452
e.presentation.icon = AutoDevStatus.WAITING.icon
12553
e.presentation.isEnabled = false
12654
return
12755
}
12856

129-
val prompting = project.service<VcsPrompting>()
130-
val changes: List<Change> = prompting.getChanges()
131-
132-
// Check if it's a GitHub repository (safe to call in BGT)
133-
isGitHubRepository = GitHubIssue.isGitHubRepository(project)
134-
135-
// Update presentation text based on whether it's a GitHub repository
136-
if (isGitHubRepository) {
137-
e.presentation.text = "Smart Commit Message (GitHub Enhanced)"
138-
e.presentation.description = "Generate commit message with AI or GitHub issue integration"
139-
} else {
140-
e.presentation.text = "Smart Commit Message"
141-
e.presentation.description = "Generate commit message with AI"
142-
}
143-
144-
// Update icon based on current job status
145-
if (currentJob?.isActive == true) {
146-
e.presentation.icon = AutoDevStatus.InProgress.icon
147-
e.presentation.text = "Cancel Commit Message Generation"
148-
e.presentation.description = "Click to cancel current generation"
149-
} else {
150-
e.presentation.icon = AutoDevStatus.Ready.icon
151-
}
57+
val prompting = e.project?.service<VcsPrompting>()
58+
val changes: List<Change> = prompting?.getChanges() ?: listOf()
15259

60+
e.presentation.icon = AutoDevStatus.Ready.icon
15361
e.presentation.isEnabled = changes.isNotEmpty()
15462
}
15563

15664
override fun executeAction(event: AnActionEvent) {
15765
val project = event.project ?: return
15866

159-
// If there's an active job, cancel it
160-
if (currentJob?.isActive == true) {
161-
currentJob?.cancel()
162-
currentJob = null
163-
AutoDevNotifications.notify(project, "Commit message generation cancelled.")
164-
return
165-
}
166-
167-
val commitMessage = getCommitMessage(event) ?: return
168-
16967
val commitWorkflowUi = VcsUtil.getCommitWorkFlowUi(event)
17068
if (commitWorkflowUi == null) {
17169
AutoDevNotifications.notify(project, "Cannot get commit workflow UI.")
@@ -178,144 +76,63 @@ class CommitMessageSuggestionAction : ChatBaseAction() {
17876
return
17977
}
18078

181-
// Store current state for later use
182-
currentChanges = changes
183-
currentEvent = event
184-
selectedIssue = null
185-
186-
// For GitHub repositories, show issue selection popup directly
187-
// For non-GitHub repositories, generate AI commit message directly
188-
if (isGitHubRepository) {
189-
generateGitHubIssueCommitMessage(project, commitMessage, event)
190-
} else {
191-
generateAICommitMessage(project, commitMessage, changes)
79+
val diffContext = project.service<VcsPrompting>().prepareContext(changes)
80+
if (diffContext.isEmpty() || diffContext == "\n") {
81+
logger.warn("Diff context is empty or cannot get enough useful context.")
82+
AutoDevNotifications.notify(project, "Diff context is empty or cannot get enough useful context.")
83+
return
19284
}
193-
}
19485

195-
private fun getCommitMessage(e: AnActionEvent) = e.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL) as? CommitMessage
86+
val editorField = (event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL) as CommitMessage).editorField
87+
val originText = editorField.editor?.selectionModel?.selectedText ?: ""
19688

197-
private fun generateGitHubIssueCommitMessage(project: Project, commitMessage: CommitMessage, event: AnActionEvent) {
198-
val task = object : Task.Backgroundable(project, "Loading GitHub issues", true) {
199-
override fun run(indicator: ProgressIndicator) {
200-
// Fix: Set indeterminate to false before setting fraction
201-
indicator.isIndeterminate = false
202-
indicator.text = "Connecting to GitHub..."
203-
indicator.fraction = 0.1
89+
currentJob?.cancel()
90+
editorField.text = ""
91+
event.presentation.icon = AutoDevStatus.InProgress.icon
92+
93+
ApplicationManager.getApplication().executeOnPooledThread {
94+
val prompt = generateCommitMessage(diffContext, project, originText)
95+
logger.info(prompt)
20496

205-
val job = AutoDevCoroutineScope.scope(project).launch {
97+
try {
98+
val stream = LlmFactory.create(project).stream(prompt, "", false)
99+
currentJob = AutoDevCoroutineScope.scope(project).launch {
206100
try {
207-
val issues = withTimeout(5000) {
208-
indicator.text = "Fetching repository issues..."
209-
indicator.fraction = 0.5
210-
fetchGitHubIssues(project)
101+
stream.cancellable().collect { chunk ->
102+
invokeLater {
103+
if (isActive) {
104+
editorField.text += chunk
105+
}
106+
}
211107
}
212-
indicator.fraction = 0.9
213-
214-
ApplicationManager.getApplication().invokeLater {
215-
if (issues.isEmpty()) {
216-
// No issues found, fall back to AI generation
217-
val changes = currentChanges ?: return@invokeLater
218-
generateAICommitMessage(project, commitMessage, changes)
219-
} else {
220-
createIssuesPopup(commitMessage, issues).showInBestPositionFor(event.dataContext)
108+
109+
val text = editorField.text
110+
if (isActive && text.startsWith("```") && text.endsWith("```")) {
111+
invokeLater {
112+
editorField.text = CodeFence.parse(text).text
113+
}
114+
} else if (isActive) {
115+
invokeLater {
116+
editorField.text = text.removePrefix("```\n").removeSuffix("```")
221117
}
222118
}
223-
} catch (ex: TimeoutCancellationException) {
224-
ApplicationManager.getApplication().invokeLater {
225-
logger.info("GitHub issues fetch timed out after 5 seconds, falling back to AI generation")
226-
AutoDevNotifications.notify(
227-
project,
228-
"GitHub connection timeout, generating commit message without issue context."
229-
)
230-
// Fall back to AI generation when timeout occurs
231-
val changes = currentChanges ?: return@invokeLater
232-
generateAICommitMessage(project, commitMessage, changes)
119+
} catch (e: Exception) {
120+
logger.error("Error during commit message generation", e)
121+
invokeLater {
122+
AutoDevNotifications.notify(project, "Error generating commit message: ${e.message}")
233123
}
234-
} catch (ex: Exception) {
235-
ApplicationManager.getApplication().invokeLater {
236-
logger.warn("Failed to fetch GitHub issues, falling back to AI generation", ex)
237-
// Fall back to AI generation when GitHub issues fetch fails
238-
val changes = currentChanges ?: return@invokeLater
239-
generateAICommitMessage(project, commitMessage, changes)
124+
} finally {
125+
invokeLater {
126+
event.presentation.icon = AutoDevStatus.Ready.icon
240127
}
241128
}
242129
}
243-
244-
runBlocking {
245-
try {
246-
job.join()
247-
} catch (ex: Exception) {
248-
logger.warn("Error waiting for GitHub issues fetch", ex)
249-
}
250-
}
130+
} catch (e: Exception) {
131+
logger.error("Failed to start commit message generation", e)
132+
event.presentation.icon = AutoDevStatus.Error.icon
133+
AutoDevNotifications.notify(project, "Failed to start commit message generation: ${e.message}")
251134
}
252135
}
253-
ProgressManager.getInstance().run(task)
254-
}
255-
256-
private fun fetchGitHubIssues(project: Project): List<IssueDisplayItem> {
257-
val ghRepository =
258-
GitHubIssue.parseGitHubRepository(project) ?: throw IllegalStateException("Not a GitHub repository")
259-
260-
// Generate cache key based on repository URL
261-
val cacheKey = "${ghRepository.url}/issues"
262-
263-
// Check cache first
264-
val cachedEntry = issuesCache[cacheKey]
265-
if (cachedEntry != null && !cachedEntry.isExpired(CACHE_TTL_MS)) {
266-
cacheHits++
267-
logger.info("Using cached GitHub issues for repository: ${ghRepository.url} (${getCacheStats()})")
268-
return cachedEntry.issues
269-
}
270-
271-
// Cache miss - fetch fresh data from GitHub API
272-
cacheMisses++
273-
logger.info("Fetching fresh GitHub issues for repository: ${ghRepository.url} (${getCacheStats()})")
274-
val issues = ghRepository.getIssues(GHIssueState.OPEN).map { issue ->
275-
val displayText = "#${issue.number} - ${issue.title}"
276-
IssueDisplayItem(issue, displayText)
277-
}
278-
279-
// Cache the results
280-
issuesCache[cacheKey] = IssueCacheEntry(issues, System.currentTimeMillis())
281-
282-
// Clean up expired entries and enforce size limit
283-
cleanupExpiredCacheEntries()
284-
enforceCacheSizeLimit()
285-
286-
return issues
287-
}
288-
289-
/**
290-
* Clean up expired cache entries to prevent memory leaks
291-
*/
292-
private fun cleanupExpiredCacheEntries() {
293-
val currentTime = System.currentTimeMillis()
294-
val expiredKeys = issuesCache.entries
295-
.filter { it.value.timestamp + CACHE_TTL_MS < currentTime }
296-
.map { it.key }
297-
298-
expiredKeys.forEach { key ->
299-
issuesCache.remove(key)
300-
logger.debug("Removed expired cache entry for key: $key")
301-
}
302-
}
303-
304-
/**
305-
* Enforce cache size limit by removing oldest entries
306-
*/
307-
private fun enforceCacheSizeLimit() {
308-
if (issuesCache.size <= MAX_CACHE_SIZE) return
309-
310-
val sortedEntries = issuesCache.entries.sortedBy { it.value.timestamp }
311-
val toRemove = sortedEntries.take(issuesCache.size - MAX_CACHE_SIZE)
312-
313-
toRemove.forEach { entry ->
314-
issuesCache.remove(entry.key)
315-
logger.debug("Removed oldest cache entry to enforce size limit: ${entry.key}")
316-
}
317-
318-
logger.info("Enforced cache size limit. Removed ${toRemove.size} entries. Current size: ${issuesCache.size}")
319136
}
320137

321138
/**
@@ -367,155 +184,6 @@ class CommitMessageSuggestionAction : ChatBaseAction() {
367184
return builder.toString()
368185
}
369186

370-
private fun createIssuesPopup(commitMessage: CommitMessage, issues: List<IssueDisplayItem>): JBPopup {
371-
var chosenIssue: IssueDisplayItem? = null
372-
return JBPopupFactory.getInstance().createPopupChooserBuilder(issues)
373-
.setTitle("Select GitHub Issue (ESC to skip)")
374-
.setVisibleRowCount(10)
375-
.setSelectionMode(SINGLE_SELECTION)
376-
.setItemSelectedCallback { chosenIssue = it }
377-
.setItemChosenCallback {
378-
chosenIssue = it
379-
}
380-
.setRenderer(object : ColoredListCellRenderer<IssueDisplayItem>() {
381-
override fun customizeCellRenderer(
382-
list: JList<out IssueDisplayItem>,
383-
value: IssueDisplayItem,
384-
index: Int,
385-
selected: Boolean,
386-
hasFocus: Boolean
387-
) {
388-
append("#${value.issue.number} ", com.intellij.ui.SimpleTextAttributes.GRAYED_ATTRIBUTES)
389-
append(value.issue.title)
390-
val labels = value.issue.labels.map { it.name }
391-
if (labels.isNotEmpty()) {
392-
append(" ")
393-
labels.forEach { label ->
394-
append("[$label] ", com.intellij.ui.SimpleTextAttributes.GRAY_ITALIC_ATTRIBUTES)
395-
}
396-
}
397-
applySpeedSearchHighlighting(list, this, true, selected)
398-
}
399-
})
400-
.addListener(object : JBPopupListener {
401-
override fun beforeShown(event: LightweightWindowEvent) {
402-
val popup = event.asPopup()
403-
val relativePoint = RelativePoint(commitMessage.editorField, Point(0, -scale(3)))
404-
val screenPoint = Point(relativePoint.screenPoint).apply { translate(0, -popup.size.height) }
405-
popup.setLocation(screenPoint)
406-
}
407-
408-
override fun onClosed(event: LightweightWindowEvent) {
409-
// IDEA-195094 Regression: New CTRL-E in "commit changes" breaks keyboard shortcuts
410-
commitMessage.editorField.requestFocusInWindow()
411-
412-
if (chosenIssue != null) {
413-
// User selected an issue
414-
handleIssueSelection(chosenIssue!!, commitMessage)
415-
} else {
416-
// User cancelled (ESC) - skip issue selection and generate with AI
417-
handleSkipIssueSelection(commitMessage)
418-
}
419-
}
420-
})
421-
.setNamerForFiltering { it.displayText }
422-
.setAutoPackHeightOnFiltering(true)
423-
.createPopup()
424-
.apply {
425-
setDataProvider { dataId ->
426-
when (dataId) {
427-
// default list action does not work as "CopyAction" is invoked first, but with other copy provider
428-
COPY_PROVIDER.name -> object : TextCopyProvider() {
429-
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
430-
override fun getTextLinesToCopy() = listOfNotNull(chosenIssue?.displayText).nullize()
431-
}
432-
433-
else -> null
434-
}
435-
}
436-
}
437-
}
438-
439-
private fun handleIssueSelection(issueItem: IssueDisplayItem, commitMessage: CommitMessage) {
440-
// Store the selected issue for AI generation
441-
selectedIssue = issueItem
442-
val project = commitMessage.editorField.project ?: return
443-
val changes = currentChanges ?: return
444-
val event = currentEvent ?: return
445-
446-
// Generate AI commit message with issue context
447-
generateAICommitMessage(project, commitMessage, changes)
448-
}
449-
450-
private fun handleSkipIssueSelection(commitMessage: CommitMessage) {
451-
// Skip issue selection, generate with AI only
452-
selectedIssue = null
453-
val project = commitMessage.editorField.project ?: return
454-
val changes = currentChanges ?: return
455-
// Generate AI commit message without issue context
456-
generateAICommitMessage(project, commitMessage, changes)
457-
}
458-
459-
private fun generateAICommitMessage(project: Project, commitMessage: CommitMessage, changes: List<Change>) {
460-
val diffContext = project.service<VcsPrompting>().prepareContext(changes)
461-
462-
if (diffContext.isEmpty() || diffContext == "\n") {
463-
logger.warn("Diff context is empty or cannot get enough useful context.")
464-
AutoDevNotifications.notify(project, "Diff context is empty or cannot get enough useful context.")
465-
return
466-
}
467-
468-
val editorField = commitMessage.editorField
469-
val originText = editorField.editor?.selectionModel?.selectedText ?: ""
470-
471-
currentJob?.cancel()
472-
editorField.text = ""
473-
474-
ApplicationManager.getApplication().executeOnPooledThread {
475-
val prompt = generateCommitMessage(diffContext, project, originText)
476-
logger.info(prompt)
477-
478-
try {
479-
val stream = LlmFactory.create(project).stream(prompt, "", false)
480-
481-
currentJob = AutoDevCoroutineScope.scope(project).launch {
482-
try {
483-
stream.cancellable().collect { chunk ->
484-
invokeLater {
485-
if (isActive) {
486-
editorField.text += chunk
487-
}
488-
}
489-
}
490-
491-
val text = editorField.text
492-
if (isActive && text.startsWith("```") && text.endsWith("```")) {
493-
invokeLater {
494-
editorField.text = CodeFence.parse(text).text
495-
}
496-
} else if (isActive) {
497-
invokeLater {
498-
editorField.text = text.removePrefix("```\n").removeSuffix("```")
499-
}
500-
}
501-
} catch (e: Exception) {
502-
logger.error("Error during commit message generation", e)
503-
invokeLater {
504-
AutoDevNotifications.notify(project, "Error generating commit message: ${e.message}")
505-
}
506-
} finally {
507-
// Job completed, will be reflected in next update() call
508-
currentJob = null
509-
}
510-
}
511-
} catch (e: Exception) {
512-
logger.error("Failed to start commit message generation", e)
513-
currentJob = null
514-
AutoDevNotifications.notify(project, "Failed to start commit message generation: ${e.message}")
515-
}
516-
}
517-
}
518-
519187
private fun generateCommitMessage(diff: String, project: Project, originText: String): String {
520188
val templateRender = TemplateRender(GENIUS_PRACTISES)
521189
val template = templateRender.getTemplate("gen-commit-msg.vm")
@@ -527,22 +195,10 @@ class CommitMessageSuggestionAction : ChatBaseAction() {
527195
""
528196
}
529197

530-
val issue = selectedIssue?.issue
531-
val issueDetail = if (issue != null) {
532-
buildString {
533-
appendLine("Title: ${issue.title}")
534-
if (!issue.body.isNullOrBlank()) {
535-
appendLine("Description: ${issue.body}")
536-
}
537-
}
538-
} else ""
539-
540198
templateRender.context = CommitMsgGenContext(
541199
historyExamples = historyExamples,
542200
diffContent = diff,
543-
originText = originText,
544-
issueId = issue?.number?.toString() ?: "",
545-
issueDetail = issueDetail
201+
originText = originText
546202
)
547203

548204
val prompter = templateRender.renderTemplate(template)
@@ -571,7 +227,4 @@ data class CommitMsgGenContext(
571227
var diffContent: String = "",
572228
// the origin commit message which is to be optimized
573229
val originText: String = "",
574-
// GitHub issue information if selected
575-
val issueId: String = "",
576-
val issueDetail: String = "",
577230
) : TemplateContext

0 commit comments

Comments
 (0)
Please sign in to comment.