mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add generate commit message feature for staged changes
- Add generateCommitMessage command to package.json with SCM menu integration - Implement git utility functions to get staged diff and files - Create command handler that uses AI to generate conventional commit messages - Add localization support for the new command - Add sparkle icon to the SCM title bar for easy access This feature allows users to generate AI-powered commit messages based on their staged changes, following conventional commit format.
This commit is contained in:
parent
b92a22b0c1
commit
4d29f89afe
5 changed files with 197 additions and 0 deletions
|
|
@ -54,6 +54,7 @@ export const commandIds = [
|
|||
"acceptInput",
|
||||
"focusPanel",
|
||||
"toggleAutoApprove",
|
||||
"generateCommitMessage",
|
||||
] as const
|
||||
|
||||
export type CommandId = (typeof commandIds)[number]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { getCommand } from "../utils/commands"
|
|||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
import { ContextProxy } from "../core/config/ContextProxy"
|
||||
import { focusPanel } from "../utils/focusPanel"
|
||||
import { getStagedDiff, getStagedFiles } from "../utils/git"
|
||||
import { getWorkspacePath } from "../utils/path"
|
||||
|
||||
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
|
||||
import { handleNewTask } from "./handleTask"
|
||||
|
|
@ -233,6 +235,119 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
|
|||
action: "toggleAutoApprove",
|
||||
})
|
||||
},
|
||||
generateCommitMessage: async () => {
|
||||
const cwd = getWorkspacePath()
|
||||
if (!cwd) {
|
||||
vscode.window.showErrorMessage("No workspace folder open")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are staged changes
|
||||
const stagedFiles = await getStagedFiles(cwd)
|
||||
if (stagedFiles.length === 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
"No staged changes found. Please stage your changes first using 'git add'.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the staged diff
|
||||
const stagedDiff = await getStagedDiff(cwd)
|
||||
if (
|
||||
stagedDiff.startsWith("Failed") ||
|
||||
stagedDiff.startsWith("Git is not installed") ||
|
||||
stagedDiff.startsWith("Not a git repository")
|
||||
) {
|
||||
vscode.window.showErrorMessage(stagedDiff)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the visible provider
|
||||
const visibleProvider = getVisibleProviderOrLog(outputChannel)
|
||||
if (!visibleProvider) {
|
||||
vscode.window.showErrorMessage("Roo Code is not active. Please open the Roo Code sidebar first.")
|
||||
return
|
||||
}
|
||||
|
||||
// Show progress notification
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Generating commit message...",
|
||||
cancellable: false,
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
// Create a task to generate the commit message
|
||||
const prompt = `Generate a concise and descriptive commit message for the following staged changes. Follow conventional commit format (e.g., feat:, fix:, docs:, style:, refactor:, test:, chore:). The message should be clear and explain what changes were made and why.
|
||||
|
||||
Staged files:
|
||||
${stagedFiles.join("\n")}
|
||||
|
||||
Diff of staged changes (truncated for context):
|
||||
${stagedDiff}
|
||||
|
||||
Please provide ONLY the commit message, without any additional explanation or formatting. The message should be on a single line unless a body is needed for complex changes.`
|
||||
|
||||
// Create a task and wait for the response
|
||||
const task = await visibleProvider.createTask(prompt)
|
||||
|
||||
// Wait for the task to complete with a simpler approach
|
||||
await new Promise<void>((resolve) => {
|
||||
let checkCount = 0
|
||||
const maxChecks = 300 // 30 seconds with 100ms intervals
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
checkCount++
|
||||
const messages = task.clineMessages
|
||||
|
||||
// Look for a message with type "say" and say "completion_result"
|
||||
const completionMessage = messages.find(
|
||||
(msg) => msg.type === "say" && msg.say === "completion_result",
|
||||
)
|
||||
|
||||
if (completionMessage || checkCount >= maxChecks) {
|
||||
clearInterval(checkInterval)
|
||||
|
||||
if (completionMessage && completionMessage.text) {
|
||||
// Extract the commit message from the completion result
|
||||
const commitMessage = completionMessage.text.trim()
|
||||
|
||||
if (commitMessage) {
|
||||
// Get the SCM input box and set the message
|
||||
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
|
||||
if (gitExtension) {
|
||||
const git = gitExtension.getAPI(1)
|
||||
const repo = git.repositories[0]
|
||||
if (repo) {
|
||||
repo.inputBox.value = commitMessage
|
||||
vscode.window.showInformationMessage(
|
||||
"Commit message generated successfully!",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (checkCount >= maxChecks) {
|
||||
vscode.window.showErrorMessage("Timeout generating commit message")
|
||||
}
|
||||
|
||||
// Clear the task
|
||||
visibleProvider.clearTask()
|
||||
resolve()
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to generate commit message: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
if (visibleProvider) {
|
||||
visibleProvider.clearTask()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
||||
|
|
|
|||
|
|
@ -179,6 +179,12 @@
|
|||
"command": "roo-cline.toggleAutoApprove",
|
||||
"title": "%command.toggleAutoApprove.title%",
|
||||
"category": "%configuration.title%"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.generateCommitMessage",
|
||||
"title": "%command.generateCommitMessage.title%",
|
||||
"category": "%configuration.title%",
|
||||
"icon": "$(sparkle)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
|
|
@ -305,6 +311,13 @@
|
|||
"group": "overflow@4",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
}
|
||||
],
|
||||
"scm/title": [
|
||||
{
|
||||
"command": "roo-cline.generateCommitMessage",
|
||||
"group": "navigation",
|
||||
"when": "scmProvider == git"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"command.terminal.explainCommand.title": "Explain This Command",
|
||||
"command.acceptInput.title": "Accept Input/Suggestion",
|
||||
"command.toggleAutoApprove.title": "Toggle Auto-Approve",
|
||||
"command.generateCommitMessage.title": "Generate Commit Message",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
|
||||
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
|
||||
|
|
|
|||
|
|
@ -355,3 +355,70 @@ export async function getWorkingState(cwd: string): Promise<string> {
|
|||
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the staged changes (diff) for commit message generation
|
||||
* @param cwd The working directory
|
||||
* @returns The staged diff or an error message
|
||||
*/
|
||||
export async function getStagedDiff(cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get the staged diff
|
||||
const { stdout: diff } = await execAsync("git diff --cached", { cwd })
|
||||
|
||||
if (!diff.trim()) {
|
||||
return "No staged changes found. Please stage your changes first using 'git add'."
|
||||
}
|
||||
|
||||
// Limit the output to prevent overwhelming the AI
|
||||
const lineLimit = GIT_OUTPUT_LINE_LIMIT
|
||||
return truncateOutput(diff, lineLimit)
|
||||
} catch (error) {
|
||||
console.error("Error getting staged diff:", error)
|
||||
return `Failed to get staged diff: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of staged files for commit message generation
|
||||
* @param cwd The working directory
|
||||
* @returns List of staged files with their status
|
||||
*/
|
||||
export async function getStagedFiles(cwd: string): Promise<string[]> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return []
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get list of staged files
|
||||
const { stdout } = await execAsync("git diff --cached --name-status", { cwd })
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
return stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
} catch (error) {
|
||||
console.error("Error getting staged files:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue