diff --git a/images/icon.png b/images/icon.png index d8ad7ba..13bae32 100644 Binary files a/images/icon.png and b/images/icon.png differ diff --git a/src/commitGenerator.ts b/src/commitGenerator.ts index 88e3731..821790d 100644 --- a/src/commitGenerator.ts +++ b/src/commitGenerator.ts @@ -1,6 +1,7 @@ import * as path from "path" import * as vscode from "vscode" -import { getGitDiffStagedFirst } from "./git" +import { getGitContext, getGitDiffStagedFirst } from "./git" +import { LATEST_PROMPT_VERSION } from "./prompts" import { createProvider } from "./providers" let abortController: AbortController | undefined @@ -99,7 +100,7 @@ async function filterReposWithChanges(repos: unknown[]): Promise { async function generateForRepository(repository: any): Promise { const repoPath = repository.rootUri.fsPath const repoName = repoPath.split(path.sep).pop() || "repository" - const diff = await getGitDiffStagedFirst(repoPath) + const { diff, stat } = await getGitContext(repoPath) await vscode.window.withProgress( { @@ -107,26 +108,26 @@ async function generateForRepository(repository: any): Promise { title: `Zemit : Génération du message de commit pour ${repoName}...`, cancellable: true, }, - (_progress, token) => performGeneration(repository.inputBox, diff, token), + (_progress, token) => performGeneration(repository.inputBox, diff, stat, token), ) } async function performGeneration( inputBox: any, diff: string, + stat: string, token: vscode.CancellationToken, ): Promise { const config = vscode.workspace.getConfiguration("zemit") const maxDiffSize = config.get("maxDiffSize", 5000) - const style = config.get("commitStyle", "conventional") + const style = config.get("promptVersion", LATEST_PROMPT_VERSION) const truncatedDiff = diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff - // Include any existing user note in the prompt const existingNote = inputBox.value?.trim() || "" - let prompt = truncatedDiff + let prompt = stat ? `## Changed files\n${stat}\n\n## Diff\n${truncatedDiff}` : truncatedDiff if (existingNote) { prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}` } diff --git a/src/git.ts b/src/git.ts index 0a60d05..ccc2b6a 100644 --- a/src/git.ts +++ b/src/git.ts @@ -43,17 +43,15 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise { return await getGitDiff(cwd, false) } } + +export async function getGitContext(cwd: string): Promise<{ diff: string; stat: string }> { + if (!(await isGitInstalled())) throw new Error("Git n'est pas installé") + if (!(await isGitRepo(cwd))) throw new Error("Pas un dépôt git") + + let diff = "" + let stat = "" + let includeUntracked = false + + if (await hasCommits(cwd)) { + const { stdout: stagedDiff } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd }) + diff = stagedDiff.trim() + + if (diff) { + const { stdout: stagedStat } = await execAsync("git --no-pager diff --staged --name-status", { cwd }) + stat = stagedStat.trim() + } else { + includeUntracked = true + const { stdout: allDiff } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd }) + diff = allDiff.trim() + const { stdout: allStat } = await execAsync("git --no-pager diff HEAD --name-status", { cwd }) + stat = allStat.trim() + } + } else { + // First commit — no HEAD exists yet + includeUntracked = true + const { stdout: cachedDiff } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd }) + diff = cachedDiff.trim() + const { stdout: cachedStat } = await execAsync("git --no-pager diff --cached --name-status", { cwd }) + stat = cachedStat.trim() + } + + if (includeUntracked) { + const { stdout: untrackedList } = await execAsync("git ls-files --others --exclude-standard", { cwd }) + const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean) + for (const file of untrackedFiles) { + const result = await execAsync( + `git --no-pager diff --no-index -- /dev/null ${JSON.stringify(file)}`, + { cwd }, + ).catch((e: any) => ({ stdout: e.stdout as string | undefined })) + if (result.stdout) diff += (diff ? "\n" : "") + result.stdout + stat += (stat ? "\n" : "") + `A\t${file}` + } + } + + diff = diff.trim() + stat = stat.trim() + + if (!diff) throw new Error("Aucune modification trouvée pour générer un message de commit") + + return { diff, stat } +} diff --git a/src/prompts.ts b/src/prompts.ts index 8ee5920..208328d 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,7 +1,9 @@ +export const LATEST_PROMPT_VERSION = "zemit-v2" + export const SYSTEM_PROMPT = "You are a git commit message generator. Output only the commit message — no explanation, no preamble, no backticks, no quotes. All messages must be written in English." -export const CONVENTIONAL_INSTRUCTION = `Based on the provided git diff, generate a concise and descriptive commit message following the Conventional Commits format: +export const ZEMIT_V1 = `Based on the provided git diff, generate a concise and descriptive commit message following the Conventional Commits format: (): @@ -23,4 +25,24 @@ Rules: - For breaking changes, add ! after the type and a BREAKING CHANGE: footer - Only include a body if there are multiple distinct changes to explain; for a single focused change, output the title only` -export const SIMPLE_INSTRUCTION = `Based on the provided git diff, generate a short and clear one-line commit message (50-72 characters).` +export const ZEMIT_V2 = `Generate a git commit message following Conventional Commits format: + +(): + +Types: feat | fix | refactor | style | docs | test | chore | perf +Scope: optional, affected module (e.g. auth, api, ui, config) +Description: lowercase, imperative mood, no trailing period + +Body rules: +- Single focused change → title only +- Multiple files or distinct concerns → title + blank line + concise bullet body listing each change +- Breaking change: add ! after type + "BREAKING CHANGE: " footer` + +const PROMPT_REGISTRY: Record = { + "zemit-v1": ZEMIT_V1, + "zemit-v2": ZEMIT_V2, +} + +export function getPrompt(version: string): string { + return PROMPT_REGISTRY[version] ?? PROMPT_REGISTRY[LATEST_PROMPT_VERSION] +} diff --git a/src/providers.ts b/src/providers.ts index 1ee9e45..f21187b 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import { SYSTEM_PROMPT, CONVENTIONAL_INSTRUCTION, SIMPLE_INSTRUCTION } from "./prompts" +import { SYSTEM_PROMPT, getPrompt } from "./prompts" export interface AIProvider { generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable @@ -15,7 +15,7 @@ class OpenAICompatibleProvider implements AIProvider { ) {} async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable { - const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION + const instruction = getPrompt(style) const url = `${this.baseUrl}/chat/completions` const response = await fetch(url, { @@ -58,7 +58,7 @@ class AnthropicProvider implements AIProvider { ) {} async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable { - const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION + const instruction = getPrompt(style) const url = `${this.baseUrl}/messages` const response = await fetch(url, {