fix: improve commit message generator quality and test coverage

- Rewrite getGitDiff tests with proper assertions (3 new tests)
- Fix markdown stripping regex to handle language-tagged code blocks
- Add --no-color flag to git diff commands for clean output
- Internationalize hardcoded error strings in setScmInputBoxMessage
- Add no_git_extension and no_git_repo i18n keys to all locales
This commit is contained in:
Roo Code 2026-03-27 00:34:48 +00:00
parent a30365d5db
commit 364941f17c
20 changed files with 123 additions and 22 deletions

View file

@ -257,6 +257,8 @@
"generating": "Generant missatge de commit...",
"no_changes": "No s'han detectat canvis. Prepara o modifica fitxers abans de generar un missatge de commit.",
"generation_failed": "No s'ha pogut generar el missatge de commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Utilitza el perfil actual",
"select_profile": "Selecciona un perfil d'API per a la generació de missatges de commit"
}

View file

@ -252,6 +252,8 @@
"generating": "Commit-Nachricht wird generiert...",
"no_changes": "Keine Änderungen erkannt. Bereite Dateien vor oder ändere sie, bevor du eine Commit-Nachricht generierst.",
"generation_failed": "Commit-Nachricht konnte nicht generiert werden",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Aktuelles Profil verwenden",
"select_profile": "Wähle ein API-Profil für die Commit-Nachricht-Generierung"
}

View file

@ -249,6 +249,8 @@
"generating": "Generating commit message...",
"no_changes": "No changes detected. Stage or modify files before generating a commit message.",
"generation_failed": "Failed to generate commit message",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Use current profile",
"select_profile": "Select an API profile for commit message generation"
}

View file

@ -252,6 +252,8 @@
"generating": "Generando mensaje de commit...",
"no_changes": "No se detectaron cambios. Prepara o modifica archivos antes de generar un mensaje de commit.",
"generation_failed": "Error al generar el mensaje de commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Usar perfil actual",
"select_profile": "Selecciona un perfil de API para la generación de mensajes de commit"
}

View file

@ -257,6 +257,8 @@
"generating": "Génération du message de commit...",
"no_changes": "Aucun changement détecté. Prépare ou modifie des fichiers avant de générer un message de commit.",
"generation_failed": "Échec de la génération du message de commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Utiliser le profil actuel",
"select_profile": "Sélectionne un profil d'API pour la génération de messages de commit"
}

View file

@ -257,6 +257,8 @@
"generating": "कमिट संदेश बनाया जा रहा है...",
"no_changes": "कोई बदलाव नहीं मिला। कमिट संदेश बनाने से पहले फ़ाइलें तैयार करें या बदलें।",
"generation_failed": "कमिट संदेश बनाने में विफल",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "वर्तमान प्रोफ़ाइल का उपयोग करें",
"select_profile": "कमिट संदेश बनाने के लिए एक API प्रोफ़ाइल चुनें"
}

View file

@ -257,6 +257,8 @@
"generating": "Membuat pesan commit...",
"no_changes": "Tidak ada perubahan terdeteksi. Siapkan atau ubah file sebelum membuat pesan commit.",
"generation_failed": "Gagal membuat pesan commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Gunakan profil saat ini",
"select_profile": "Pilih profil API untuk pembuatan pesan commit"
}

View file

@ -257,6 +257,8 @@
"generating": "Generazione del messaggio di commit...",
"no_changes": "Nessuna modifica rilevata. Prepara o modifica i file prima di generare un messaggio di commit.",
"generation_failed": "Impossibile generare il messaggio di commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Usa il profilo corrente",
"select_profile": "Seleziona un profilo API per la generazione del messaggio di commit"
}

View file

@ -257,6 +257,8 @@
"generating": "コミットメッセージを生成中...",
"no_changes": "変更が検出されませんでした。コミットメッセージを生成する前にファイルをステージするか変更してください。",
"generation_failed": "コミットメッセージの生成に失敗しました",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "現在のプロファイルを使用",
"select_profile": "コミットメッセージ生成用のAPIプロファイルを選択"
}

View file

@ -257,6 +257,8 @@
"generating": "커밋 메시지 생성 중...",
"no_changes": "변경 사항이 없습니다. 커밋 메시지를 생성하기 전에 파일을 스테이지하거나 수정하세요.",
"generation_failed": "커밋 메시지 생성에 실패했습니다",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "현재 프로필 사용",
"select_profile": "커밋 메시지 생성에 사용할 API 프로필을 선택하세요"
}

View file

@ -257,6 +257,8 @@
"generating": "Commitbericht genereren...",
"no_changes": "Geen wijzigingen gedetecteerd. Stage of wijzig bestanden voordat je een commitbericht genereert.",
"generation_failed": "Kan commitbericht niet genereren",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Huidig profiel gebruiken",
"select_profile": "Selecteer een API-profiel voor het genereren van commitberichten"
}

View file

@ -257,6 +257,8 @@
"generating": "Generowanie wiadomości commita...",
"no_changes": "Nie wykryto zmian. Przygotuj lub zmodyfikuj pliki przed wygenerowaniem wiadomości commita.",
"generation_failed": "Nie udało się wygenerować wiadomości commita",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Użyj bieżącego profilu",
"select_profile": "Wybierz profil API do generowania wiadomości commita"
}

View file

@ -257,6 +257,8 @@
"generating": "Gerando mensagem de commit...",
"no_changes": "Nenhuma alteração detectada. Prepare ou modifique arquivos antes de gerar uma mensagem de commit.",
"generation_failed": "Falha ao gerar mensagem de commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Usar perfil atual",
"select_profile": "Selecione um perfil de API para a geração de mensagens de commit"
}

View file

@ -257,6 +257,8 @@
"generating": "Генерация сообщения коммита...",
"no_changes": "Изменения не обнаружены. Подготовьте или измените файлы перед генерацией сообщения коммита.",
"generation_failed": "Не удалось сгенерировать сообщение коммита",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Использовать текущий профиль",
"select_profile": "Выберите профиль API для генерации сообщения коммита"
}

View file

@ -257,6 +257,8 @@
"generating": "Commit mesajı oluşturuluyor...",
"no_changes": "Değişiklik algılanmadı. Commit mesajı oluşturmadan önce dosyaları hazırla veya değiştir.",
"generation_failed": "Commit mesajı oluşturulamadı",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Mevcut profili kullan",
"select_profile": "Commit mesajı oluşturma için bir API profili seç"
}

View file

@ -264,6 +264,8 @@
"generating": "Đang tạo thông điệp commit...",
"no_changes": "Không phát hiện thay đổi nào. Hãy chuẩn bị hoặc chỉnh sửa tệp trước khi tạo thông điệp commit.",
"generation_failed": "Không thể tạo thông điệp commit",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "Sử dụng hồ sơ hiện tại",
"select_profile": "Chọn một hồ sơ API để tạo thông điệp commit"
}

View file

@ -262,6 +262,8 @@
"generating": "正在生成提交信息...",
"no_changes": "未检测到更改。请在生成提交信息之前暂存或修改文件。",
"generation_failed": "生成提交信息失败",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "使用当前配置",
"select_profile": "选择用于生成提交信息的 API 配置"
}

View file

@ -257,6 +257,8 @@
"generating": "正在產生提交訊息...",
"no_changes": "未偵測到變更。請在產生提交訊息之前暫存或修改檔案。",
"generation_failed": "產生提交訊息失敗",
"no_git_extension": "Git extension is not available.",
"no_git_repo": "No git repository found.",
"use_current_profile": "使用目前的設定檔",
"select_profile": "選擇用於產生提交訊息的 API 設定檔"
}

View file

@ -3,12 +3,34 @@ import * as singleCompletionHandlerModule from "../single-completion-handler"
import type { ProviderSettings } from "@roo-code/types"
vi.mock("../single-completion-handler")
vi.mock("child_process")
// Mock child_process.exec to simulate git commands
const mockExecImpl = vi.fn()
vi.mock("child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("child_process")>()
return {
...actual,
exec: (...args: any[]) => mockExecImpl(...args),
}
})
vi.mock("util", async (importOriginal) => {
const actual = await importOriginal<typeof import("util")>()
return {
...actual,
promisify: vi.fn((fn: any) => fn),
promisify:
(fn: any) =>
(...args: any[]) =>
new Promise((resolve, reject) => {
fn(...args, (err: Error | null, result: any) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
}),
}
})
@ -57,6 +79,14 @@ describe("commit-message-generator", () => {
expect(result).toBe("feat: add feature")
})
it("strips language-tagged markdown code blocks from the result", async () => {
mockSingleCompletionHandler.mockResolvedValue("```text\nfix: resolve null check\n```")
const result = await generateCommitMessageFromDiff(mockApiConfig, "some diff")
expect(result).toBe("fix: resolve null check")
})
it("propagates errors from the completion handler", async () => {
mockSingleCompletionHandler.mockRejectedValue(new Error("API Error"))
@ -66,28 +96,62 @@ describe("commit-message-generator", () => {
describe("getGitDiff", () => {
it("returns staged diff when available", async () => {
const { exec } = await import("child_process")
const mockExec = vi.mocked(exec) as any
mockExec.mockImplementation(
mockExecImpl.mockImplementation(
(
cmd: string,
_opts: any,
callback?: (err: Error | null, result: { stdout: string; stderr: string }) => void,
_opts: unknown,
callback: (err: Error | null, result: { stdout: string; stderr: string }) => void,
) => {
if (callback) {
if (cmd === "git diff --cached") {
callback(null, { stdout: "staged changes", stderr: "" })
} else {
callback(null, { stdout: "", stderr: "" })
}
if (cmd.includes("--cached")) {
callback(null, { stdout: "staged changes", stderr: "" })
} else {
callback(null, { stdout: "unstaged changes", stderr: "" })
}
return { stdout: cmd === "git diff --cached" ? "staged changes" : "", stderr: "" }
},
)
// Since we mock promisify, exec is already "promisified" via our mock
// The actual function uses execAsync which is promisify(exec)
// We need to test the logic differently since promisify is mocked
const result = await getGitDiff("/workspace")
expect(result).toBe("staged changes")
expect(mockExecImpl).toHaveBeenCalledWith(
"git diff --cached --no-color",
expect.objectContaining({ cwd: "/workspace" }),
expect.any(Function),
)
})
it("falls back to unstaged diff when nothing is staged", async () => {
mockExecImpl.mockImplementation(
(
cmd: string,
_opts: unknown,
callback: (err: Error | null, result: { stdout: string; stderr: string }) => void,
) => {
if (cmd.includes("--cached")) {
callback(null, { stdout: "", stderr: "" })
} else {
callback(null, { stdout: "unstaged changes", stderr: "" })
}
},
)
const result = await getGitDiff("/workspace")
expect(result).toBe("unstaged changes")
})
it("throws an error when git command fails", async () => {
mockExecImpl.mockImplementation(
(
_cmd: string,
_opts: unknown,
callback: (err: Error | null, result: { stdout: string; stderr: string }) => void,
) => {
callback(new Error("git not found"), { stdout: "", stderr: "" })
},
)
await expect(getGitDiff("/workspace")).rejects.toThrow("Failed to get git diff")
})
})
})

View file

@ -5,6 +5,7 @@ import { promisify } from "util"
import type { ProviderSettings } from "@roo-code/types"
import { singleCompletionHandler } from "./single-completion-handler"
import { t } from "../i18n"
const execAsync = promisify(exec)
@ -30,7 +31,7 @@ Git diff:
export async function getGitDiff(workspaceRoot: string): Promise<string> {
try {
// Try staged changes first
const { stdout: stagedDiff } = await execAsync("git diff --cached", {
const { stdout: stagedDiff } = await execAsync("git diff --cached --no-color", {
cwd: workspaceRoot,
maxBuffer: 1024 * 1024,
})
@ -40,7 +41,7 @@ export async function getGitDiff(workspaceRoot: string): Promise<string> {
}
// Fall back to unstaged changes
const { stdout: unstagedDiff } = await execAsync("git diff", {
const { stdout: unstagedDiff } = await execAsync("git diff --no-color", {
cwd: workspaceRoot,
maxBuffer: 1024 * 1024,
})
@ -64,7 +65,7 @@ export async function generateCommitMessageFromDiff(apiConfiguration: ProviderSe
// Clean up the result - remove any markdown formatting the model might add
return result
.replace(/^```[\s\S]*?\n/, "")
.replace(/^```[^\n]*\n/, "")
.replace(/\n```$/, "")
.trim()
}
@ -87,7 +88,7 @@ export async function setScmInputBoxMessage(message: string): Promise<boolean> {
const gitExtension = vscode.extensions.getExtension("vscode.git")
if (!gitExtension) {
vscode.window.showErrorMessage("Git extension is not available.")
vscode.window.showErrorMessage(t("common:commit.no_git_extension"))
return false
}
@ -95,7 +96,7 @@ export async function setScmInputBoxMessage(message: string): Promise<boolean> {
const api = git.getAPI(1)
if (!api || api.repositories.length === 0) {
vscode.window.showErrorMessage("No git repository found.")
vscode.window.showErrorMessage(t("common:commit.no_git_repo"))
return false
}