Refactor: Git commit message generation as a module (#5031)

* Refactor: Git commit message generation as a module

Implement Git commit message generation as a module

Refactors the Git commit message generation functionality into a separate module for better organization and maintainability.

The changes include:

- Moving the commit message generation logic from `src/core/controller/index.ts` to a new module `src/integrations/git/commit-message-generator.ts`.
- Adding a command to abort commit message generation.
- Updating the `cline.generateGitCommitMessage` command to use the new module.
- Adding a context key to disable the generate commit message button when a commit is generating.

This refactoring improves the codebase by:

- Separating concerns: The commit message generation logic is now isolated in its own module, making it easier to understand and maintain.
- Improving testability: The new module can be tested independently of the controller.
- Promoting code reuse: The commit message generation logic can be reused in other parts of the application if needed.

* clean up

* changeset added
This commit is contained in:
Bee
2025-07-21 10:43:14 -07:00
committed by GitHub
parent e7ce38bd85
commit c970b8030e
5 changed files with 148 additions and 170 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor Git commit message generation to support output streaming.
+18 -3
View File
@@ -186,6 +186,12 @@
"category": "Cline",
"icon": "$(robot)"
},
{
"command": "cline.abortGitCommitMessage",
"title": "Generate Commit Message with Cline - Stop",
"category": "Cline",
"icon": "$(debug-stop)"
},
{
"command": "cline.explainCode",
"title": "Explain with Cline",
@@ -213,7 +219,7 @@
},
{
"command": "cline.generateGitCommitMessage",
"when": "scmProvider == git"
"when": "config.git.enabled && scmProvider == git"
},
{
"command": "cline.focusChatInput",
@@ -306,13 +312,22 @@
{
"command": "cline.generateGitCommitMessage",
"group": "navigation",
"when": "scmProvider == git"
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
},
{
"command": "cline.abortGitCommitMessage",
"group": "navigation",
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
}
],
"commandPalette": [
{
"command": "cline.generateGitCommitMessage",
"when": "scmProvider == git"
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
},
{
"command": "cline.abortGitCommitMessage",
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
}
]
},
+2 -132
View File
@@ -34,7 +34,7 @@ import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { AuthService } from "@/services/auth/AuthService"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { clineEnvConfig } from "@/config"
@@ -852,135 +852,5 @@ export class Controller {
// secrets
// Git commit message generation
async generateGitCommitMessage() {
try {
// Check if there's a workspace folder open
const cwd = await getCwd()
if (!cwd) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
})
return
}
// Get the git diff
const gitDiff = await getWorkingState(cwd)
if (gitDiff === "No changes in working directory") {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
})
return
}
// Show a progress notification
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Generating commit message...",
cancellable: false,
},
async (progress, token) => {
try {
// Format the git diff into a prompt
const prompt = `Based on the following git diff, generate a concise and descriptive commit message:
${gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff}
The commit message should:
1. Start with a short summary (50-72 characters)
2. Use the imperative mood (e.g., "Add feature" not "Added feature")
3. Describe what was changed and why
4. Be clear and descriptive
Commit message:`
// Get the current API configuration
const { apiConfiguration } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
// Build the API handler
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
// Create a system prompt
const systemPrompt =
"You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs."
// Create a message for the API
const messages = [
{
role: "user" as const,
content: prompt,
},
]
// Call the API directly
const stream = apiHandler.createMessage(systemPrompt, messages)
// Collect the response
let response = ""
for await (const chunk of stream) {
if (chunk.type === "text") {
response += chunk.text
}
}
// Extract the commit message
const commitMessage = extractCommitMessage(response)
// Apply the commit message to the Git input box
if (commitMessage) {
// Get the Git extension API
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
if (gitExtension) {
const api = gitExtension.getAPI(1)
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = commitMessage
const message = "Commit message generated and applied"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
} else {
const message = "No Git repositories found"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} else {
const message = "Git extension not found"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} else {
const message = "Failed to generate commit message"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} catch (innerError) {
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
})
}
},
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
})
}
}
// dev
}
+7 -17
View File
@@ -10,7 +10,6 @@ import { DIFF_VIEW_URI_SCHEME } from "@hosts/vscode/VscodeDiffViewProvider"
import assert from "node:assert"
import { posthogClientProvider } from "./services/posthog/PostHogClientProvider"
import { WebviewProvider } from "./core/webview"
import { Controller } from "./core/controller"
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
import { ErrorService } from "./services/error/ErrorService"
@@ -40,7 +39,8 @@ import { AuthService } from "./services/auth/AuthService"
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
import { getHostBridgeProvider } from "@hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
import { ShowMessageType } from "./shared/proto/host/window"
import { GitCommitGenerator } from "./integrations/git/commit-message-generator"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -666,21 +666,11 @@ export async function activate(context: vscode.ExtensionContext) {
// Register the generateGitCommitMessage command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.generateGitCommitMessage", async () => {
// Get the controller from any instance, without activating the view
const controller = WebviewProvider.getAllInstances()[0]?.controller
if (controller) {
// Call the controller method to generate commit message
await controller.generateGitCommitMessage()
} else {
// Create a temporary controller just for this operation
const outputChannel = vscode.window.createOutputChannel("Cline Commit Generator")
const tempController = new Controller(context, outputChannel, () => Promise.resolve(true), uuidv4())
await tempController.generateGitCommitMessage()
outputChannel.dispose()
}
vscode.commands.registerCommand("cline.generateGitCommitMessage", async (scm) => {
await GitCommitGenerator?.generate?.(context, scm)
}),
vscode.commands.registerCommand("cline.abortGitCommitMessage", () => {
GitCommitGenerator?.abort?.()
}),
)
+116 -18
View File
@@ -1,7 +1,116 @@
import * as vscode from "vscode"
import { writeTextToClipboard } from "@utils/env"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window"
import { ShowMessageType, ShowTextDocumentRequest } from "@/shared/proto/host/window"
import { buildApiHandler } from "@/api"
import { getAllExtensionState } from "@/core/storage/state"
import { getWorkingState } from "@/utils/git"
import { getCwd } from "@/utils/path"
/**
* Git commit message generator module
*/
export const GitCommitGenerator = {
generate,
abort,
}
let commitGenerationAbortController: AbortController | undefined = undefined
async function generate(context: vscode.ExtensionContext, scm?: vscode.SourceControl) {
const cwd = await getCwd()
if (!context || !cwd) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
})
return
}
const gitDiff = await getWorkingState(cwd)
if (gitDiff === "No changes in working directory") {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
})
return
}
const inputBox = scm?.inputBox
if (!inputBox) {
vscode.window.showErrorMessage("Git extension not found or no repositories available")
return
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.SourceControl,
title: "Generating commit message...",
cancellable: true,
},
() => performCommitGeneration(context, gitDiff, inputBox),
)
}
async function performCommitGeneration(context: vscode.ExtensionContext, gitDiff: string, inputBox: any) {
try {
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", true)
const truncatedDiff = gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff
const prompt = `Based on the following git diff, generate a concise and descriptive commit message:
${truncatedDiff}
The commit message should:
1. Start with a short summary (50-72 characters)
2. Use the imperative mood (e.g., "Add feature" not "Added feature")
3. Describe what was changed and why
4. Be clear and descriptive
Commit message:`
// Get the current API configuration
const { apiConfiguration } = await getAllExtensionState(context)
// Set to use Act mode for now by default
// TODO: A new mode for commit generation
const currentMode = "act"
// Build the API handler
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
// Create a system prompt
const systemPrompt =
"You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs."
// Create a message for the API
const messages = [{ role: "user" as const, content: prompt }]
commitGenerationAbortController = new AbortController()
const stream = apiHandler.createMessage(systemPrompt, messages)
let response = ""
for await (const chunk of stream) {
commitGenerationAbortController.signal.throwIfAborted()
if (chunk.type === "text") {
response += chunk.text
inputBox.value = extractCommitMessage(response)
}
}
if (!inputBox.value) {
throw new Error("empty API response")
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
} finally {
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false)
}
}
function abort() {
commitGenerationAbortController?.abort()
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false)
}
/**
* Formats the git diff into a prompt for the AI
* @param gitDiff The git diff to format
@@ -31,26 +140,15 @@ Commit message:`
/**
* Extracts the commit message from the AI response
* @param aiResponse The response from the AI
* @param str String containing the AI response
* @returns The extracted commit message
*/
export function extractCommitMessage(aiResponse: string): string {
export function extractCommitMessage(str: string): string {
// Remove any markdown formatting or extra text
let message = aiResponse.trim()
// Remove markdown code blocks if present
if (message.startsWith("```") && message.endsWith("```")) {
message = message.substring(3, message.length - 3).trim()
// Remove language identifier if present (e.g., ```git)
const firstLineBreak = message.indexOf("\n")
if (firstLineBreak > 0 && firstLineBreak < 20) {
// Reasonable length for a language identifier
message = message.substring(firstLineBreak).trim()
}
}
return message
return str
.trim()
.replace(/^```[^\n]*\n?|```$/g, "")
.trim()
}
/**