mirror of
https://github.com/cline/cline.git
synced 2026-10-30 18:12:49 +08:00
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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactor Git commit message generation to support output streaming.
|
||||
+18
-3
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
@@ -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?.()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user