Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e309fa3acd | |||
| c870350c11 | |||
| f823dc4803 | |||
| cbceea72fa | |||
| 4471c2b2b2 | |||
| f5f6a2f703 |
+7
-3
@@ -1,6 +1,10 @@
|
|||||||
src/**
|
.vscode/**
|
||||||
node_modules/**
|
.git/**
|
||||||
.gitignore
|
.gitignore
|
||||||
|
node_modules/**
|
||||||
|
*.vsix
|
||||||
|
assets/**
|
||||||
|
src/**
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
cline-main/**
|
|
||||||
**/*.map
|
**/*.map
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Génère des messages de commit, via un modèle d'IA, directement dans VSCode.
|
Génère des messages de commit, via un modèle d'IA, directement dans VSCode.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Ce que ça fait
|
## Ce que ça fait
|
||||||
|
|
||||||
Zemit lit le diff stagé dans ton dépôt Git et envoie le contenu à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control.
|
Zemit lit le diff stagé et le résumé des fichiers modifiés dans ton dépôt Git, puis envoie ce contexte à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control.
|
||||||
|
|
||||||
Tu peux interrompre la génération à tout moment depuis le même panneau.
|
Tu peux interrompre la génération à tout moment depuis le même panneau.
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ Les paramètres se trouvent dans les préférences VS Code sous **Zemit**.
|
|||||||
| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
|
| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ |
|
||||||
| `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` |
|
| `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` |
|
||||||
| `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
|
| `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
|
||||||
| `zemit.commitStyle` | Style du message : `conventional` ou `simple` | `conventional` |
|
| `zemit.promptVersion` | Version du prompt | `zemit-v2` |
|
||||||
| `zemit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` |
|
| `zemit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` |
|
||||||
|
|
||||||
Pour choisir un modèle parmi ceux disponibles chez ton fournisseur, lance la commande **Zemit: Select Model** depuis la palette de commandes.
|
Pour choisir un modèle parmi ceux disponibles chez ton fournisseur, lance la commande **Zemit: Select Model** depuis la palette de commandes.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.2 KiB |
Generated
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.0.0",
|
"@types/node": "^18.0.0",
|
||||||
@@ -1038,9 +1038,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vscode/vsce": {
|
"node_modules/@vscode/vsce": {
|
||||||
"version": "3.8.1",
|
"version": "3.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz",
|
||||||
"integrity": "sha512-Ij1i53rvR2Z/BR8tdESNqb5l5GNvOLQIWSbE1NnRnXQrvJu/xhK8nVfe6vXKdI6L7/fUwzlqqB1gOjt901mTmg==",
|
"integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1078,7 +1078,7 @@
|
|||||||
"vsce": "vsce"
|
"vsce": "vsce"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 20"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"keytar": "^7.7.0"
|
"keytar": "^7.7.0"
|
||||||
|
|||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zemit",
|
"name": "zemit",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"displayName": "Zemit",
|
"displayName": "Zemit",
|
||||||
"description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.",
|
"description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -56,18 +56,18 @@
|
|||||||
"default": "",
|
"default": "",
|
||||||
"markdownDescription": "URL de base personnalisée (ex. `http://localhost:11434/v1` pour Ollama). Laisser vide pour utiliser la valeur par défaut du fournisseur."
|
"markdownDescription": "URL de base personnalisée (ex. `http://localhost:11434/v1` pour Ollama). Laisser vide pour utiliser la valeur par défaut du fournisseur."
|
||||||
},
|
},
|
||||||
"zemit.commitStyle": {
|
"zemit.promptVersion": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"conventional",
|
"zemit-v1",
|
||||||
"simple"
|
"zemit-v2"
|
||||||
],
|
],
|
||||||
"enumDescriptions": [
|
"enumDescriptions": [
|
||||||
"Format Conventional Commits (feat:, fix:, chore:, etc.)",
|
"Zemit V1 – Format Conventional Commits classique",
|
||||||
"Description courte sur une ligne"
|
"Zemit V2 – Conventional Commits compact avec résumé de fichiers et corps structuré"
|
||||||
],
|
],
|
||||||
"default": "conventional",
|
"default": "zemit-v2",
|
||||||
"description": "Format du message de commit généré."
|
"description": "Version du prompt utilisé pour générer le message de commit."
|
||||||
},
|
},
|
||||||
"zemit.maxDiffSize": {
|
"zemit.maxDiffSize": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { getGitDiffStagedFirst } from "./git"
|
import { getGitContext, getGitDiffStagedFirst } from "./git"
|
||||||
|
import { LATEST_PROMPT_VERSION } from "./prompts"
|
||||||
import { createProvider } from "./providers"
|
import { createProvider } from "./providers"
|
||||||
|
|
||||||
let abortController: AbortController | undefined
|
let abortController: AbortController | undefined
|
||||||
@@ -99,7 +100,7 @@ async function filterReposWithChanges(repos: unknown[]): Promise<unknown[]> {
|
|||||||
async function generateForRepository(repository: any): Promise<void> {
|
async function generateForRepository(repository: any): Promise<void> {
|
||||||
const repoPath = repository.rootUri.fsPath
|
const repoPath = repository.rootUri.fsPath
|
||||||
const repoName = repoPath.split(path.sep).pop() || "repository"
|
const repoName = repoPath.split(path.sep).pop() || "repository"
|
||||||
const diff = await getGitDiffStagedFirst(repoPath)
|
const { diff, stat } = await getGitContext(repoPath)
|
||||||
|
|
||||||
await vscode.window.withProgress(
|
await vscode.window.withProgress(
|
||||||
{
|
{
|
||||||
@@ -107,26 +108,26 @@ async function generateForRepository(repository: any): Promise<void> {
|
|||||||
title: `Zemit : Génération du message de commit pour ${repoName}...`,
|
title: `Zemit : Génération du message de commit pour ${repoName}...`,
|
||||||
cancellable: true,
|
cancellable: true,
|
||||||
},
|
},
|
||||||
(_progress, token) => performGeneration(repository.inputBox, diff, token),
|
(_progress, token) => performGeneration(repository.inputBox, diff, stat, token),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performGeneration(
|
async function performGeneration(
|
||||||
inputBox: any,
|
inputBox: any,
|
||||||
diff: string,
|
diff: string,
|
||||||
|
stat: string,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = vscode.workspace.getConfiguration("zemit")
|
const config = vscode.workspace.getConfiguration("zemit")
|
||||||
const maxDiffSize = config.get<number>("maxDiffSize", 5000)
|
const maxDiffSize = config.get<number>("maxDiffSize", 5000)
|
||||||
const style = config.get<string>("commitStyle", "conventional")
|
const style = config.get<string>("promptVersion", LATEST_PROMPT_VERSION)
|
||||||
|
|
||||||
const truncatedDiff =
|
const truncatedDiff =
|
||||||
diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff
|
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() || ""
|
const existingNote = inputBox.value?.trim() || ""
|
||||||
|
|
||||||
let prompt = truncatedDiff
|
let prompt = stat ? `## Changed files\n${stat}\n\n## Diff\n${truncatedDiff}` : truncatedDiff
|
||||||
if (existingNote) {
|
if (existingNote) {
|
||||||
prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}`
|
prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}`
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-10
@@ -43,17 +43,15 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise<strin
|
|||||||
if (await hasCommits(cwd)) {
|
if (await hasCommits(cwd)) {
|
||||||
const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd })
|
const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd })
|
||||||
diff = staged.trim()
|
diff = staged.trim()
|
||||||
}
|
|
||||||
|
|
||||||
if (!stagedOnly && !diff) {
|
if (!stagedOnly && !diff) {
|
||||||
const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd })
|
const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd })
|
||||||
diff = unstaged.trim()
|
diff = unstaged.trim()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// New repo with no commits yet — show everything that's staged/tracked
|
// First commit — no HEAD exists yet
|
||||||
if (!diff) {
|
const { stdout: cached } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
|
||||||
const { stdout: newRepo } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd })
|
diff = cached.trim()
|
||||||
diff = newRepo.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include untracked (new) files when not limiting to staged only
|
// Include untracked (new) files when not limiting to staged only
|
||||||
@@ -85,3 +83,55 @@ export async function getGitDiffStagedFirst(cwd: string): Promise<string> {
|
|||||||
return await getGitDiff(cwd, false)
|
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 }
|
||||||
|
}
|
||||||
|
|||||||
+24
-2
@@ -1,7 +1,9 @@
|
|||||||
|
export const LATEST_PROMPT_VERSION = "zemit-v2"
|
||||||
|
|
||||||
export const SYSTEM_PROMPT =
|
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."
|
"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:
|
||||||
|
|
||||||
<type>(<scope>): <short description>
|
<type>(<scope>): <short description>
|
||||||
|
|
||||||
@@ -23,4 +25,24 @@ Rules:
|
|||||||
- For breaking changes, add ! after the type and a BREAKING CHANGE: footer
|
- 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`
|
- 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:
|
||||||
|
|
||||||
|
<type>(<scope>): <short description>
|
||||||
|
|
||||||
|
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: <detail>" footer`
|
||||||
|
|
||||||
|
const PROMPT_REGISTRY: Record<string, string> = {
|
||||||
|
"zemit-v1": ZEMIT_V1,
|
||||||
|
"zemit-v2": ZEMIT_V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrompt(version: string): string {
|
||||||
|
return PROMPT_REGISTRY[version] ?? PROMPT_REGISTRY[LATEST_PROMPT_VERSION]
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { SYSTEM_PROMPT, CONVENTIONAL_INSTRUCTION, SIMPLE_INSTRUCTION } from "./prompts"
|
import { SYSTEM_PROMPT, getPrompt } from "./prompts"
|
||||||
|
|
||||||
export interface AIProvider {
|
export interface AIProvider {
|
||||||
generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string>
|
generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string>
|
||||||
@@ -15,7 +15,7 @@ class OpenAICompatibleProvider implements AIProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
|
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
|
||||||
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION
|
const instruction = getPrompt(style)
|
||||||
const url = `${this.baseUrl}/chat/completions`
|
const url = `${this.baseUrl}/chat/completions`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -58,7 +58,7 @@ class AnthropicProvider implements AIProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
|
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
|
||||||
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION
|
const instruction = getPrompt(style)
|
||||||
const url = `${this.baseUrl}/messages`
|
const url = `${this.baseUrl}/messages`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|||||||
Reference in New Issue
Block a user