From 364941f17ca6b2b42b00e64c18d2e7237f8067f2 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 27 Mar 2026 00:34:48 +0000 Subject: [PATCH] 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 --- src/i18n/locales/ca/common.json | 2 + src/i18n/locales/de/common.json | 2 + src/i18n/locales/en/common.json | 2 + src/i18n/locales/es/common.json | 2 + src/i18n/locales/fr/common.json | 2 + src/i18n/locales/hi/common.json | 2 + src/i18n/locales/id/common.json | 2 + src/i18n/locales/it/common.json | 2 + src/i18n/locales/ja/common.json | 2 + src/i18n/locales/ko/common.json | 2 + src/i18n/locales/nl/common.json | 2 + src/i18n/locales/pl/common.json | 2 + src/i18n/locales/pt-BR/common.json | 2 + src/i18n/locales/ru/common.json | 2 + src/i18n/locales/tr/common.json | 2 + src/i18n/locales/vi/common.json | 2 + src/i18n/locales/zh-CN/common.json | 2 + src/i18n/locales/zh-TW/common.json | 2 + .../commit-message-generator.spec.ts | 98 +++++++++++++++---- src/utils/commit-message-generator.ts | 11 ++- 20 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 8f2002f61a..48ec149499 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -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" } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 98de7c6d53..8591ff4002 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -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" } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 40f54074e8..51aaadc43e 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -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" } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index dde75ee81f..a533653a6d 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -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" } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index ef3932bcd5..f768fb7d0b 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -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" } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index c6468439f3..ceb579ea7b 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -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 प्रोफ़ाइल चुनें" } diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 06b13a0feb..243ef822f9 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -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" } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 3d9840e2f8..8e7e722392 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -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" } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 44ffa3cb5d..ae92ab3bb9 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -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プロファイルを選択" } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 0aa495f267..aec996a38c 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -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 프로필을 선택하세요" } diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 8c4a381916..bd1294f147 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -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" } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index db6cbb7376..90b4f1860b 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -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" } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index baae080768..2e256bd8af 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -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" } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index bf7f1d93fe..71a04ae742 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -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 для генерации сообщения коммита" } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 46e8adb9a2..b00a3a5f35 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -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ç" } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 856d5e748a..0f18f74d2c 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -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" } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index b205ecebbe..047727812d 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -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 配置" } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 0400390c1c..2ddb6a92a3 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -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 設定檔" } diff --git a/src/utils/__tests__/commit-message-generator.spec.ts b/src/utils/__tests__/commit-message-generator.spec.ts index 87108913c1..edc9d50ddf 100644 --- a/src/utils/__tests__/commit-message-generator.spec.ts +++ b/src/utils/__tests__/commit-message-generator.spec.ts @@ -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() + return { + ...actual, + exec: (...args: any[]) => mockExecImpl(...args), + } +}) + vi.mock("util", async (importOriginal) => { const actual = await importOriginal() 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") }) }) }) diff --git a/src/utils/commit-message-generator.ts b/src/utils/commit-message-generator.ts index 3429abaabd..b4a2ec7021 100644 --- a/src/utils/commit-message-generator.ts +++ b/src/utils/commit-message-generator.ts @@ -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 { 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 { } // 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 { 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 { 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 }