浏览代码

fix: add Git installation check for checkpoints feature (#3109) (#5920)

Co-authored-by: Daniel Riccio <[email protected]>
Murilo Pires 5 月之前
父节点
当前提交
0cb76d9ae9

+ 46 - 4
src/core/checkpoints/index.ts

@@ -6,6 +6,8 @@ import { TelemetryService } from "@roo-code/telemetry"
 import { Task } from "../task/Task"
 
 import { getWorkspacePath } from "../../utils/path"
+import { checkGitInstalled } from "../../utils/git"
+import { t } from "../../i18n"
 
 import { ClineApiReqInfo } from "../../shared/ExtensionMessage"
 import { getApiMetrics } from "../../shared/getApiMetrics"
@@ -70,6 +72,47 @@ export function getCheckpointService(cline: Task) {
 
 		cline.checkpointServiceInitializing = true
 
+		// Check if Git is installed before initializing the service
+		// Note: This is intentionally fire-and-forget to match the original IIFE pattern
+		// The service is returned immediately while Git check happens asynchronously
+		checkGitInstallation(cline, service, log, provider)
+
+		return service
+	} catch (err) {
+		log(`[Task#getCheckpointService] ${err.message}`)
+		cline.enableCheckpoints = false
+		return undefined
+	}
+}
+
+async function checkGitInstallation(
+	cline: Task,
+	service: RepoPerTaskCheckpointService,
+	log: (message: string) => void,
+	provider: any,
+) {
+	try {
+		const gitInstalled = await checkGitInstalled()
+
+		if (!gitInstalled) {
+			log("[Task#getCheckpointService] Git is not installed, disabling checkpoints")
+			cline.enableCheckpoints = false
+			cline.checkpointServiceInitializing = false
+
+			// Show user-friendly notification
+			const selection = await vscode.window.showWarningMessage(
+				t("common:errors.git_not_installed"),
+				t("common:buttons.learn_more"),
+			)
+
+			if (selection === t("common:buttons.learn_more")) {
+				await vscode.env.openExternal(vscode.Uri.parse("https://git-scm.com/downloads"))
+			}
+
+			return
+		}
+
+		// Git is installed, proceed with initialization
 		service.on("initialize", () => {
 			log("[Task#getCheckpointService] service initialized")
 
@@ -115,12 +158,11 @@ export function getCheckpointService(cline: Task) {
 			log(`[Task#getCheckpointService] initShadowGit -> ${err.message}`)
 			cline.enableCheckpoints = false
 		})
-
-		return service
 	} catch (err) {
-		log(`[Task#getCheckpointService] ${err.message}`)
+		log(`[Task#getCheckpointService] Unexpected error during Git check: ${err.message}`)
+		console.error("Git check error:", err)
 		cline.enableCheckpoints = false
-		return undefined
+		cline.checkpointServiceInitializing = false
 	}
 }
 

+ 3 - 1
src/i18n/locales/ca/common.json

@@ -32,6 +32,7 @@
 		"could_not_open_file_generic": "No s'ha pogut obrir el fitxer!",
 		"checkpoint_timeout": "S'ha esgotat el temps en intentar restaurar el punt de control.",
 		"checkpoint_failed": "Ha fallat la restauració del punt de control.",
+		"git_not_installed": "Git és necessari per a la funció de punts de control. Si us plau, instal·la Git per activar els punts de control.",
 		"no_workspace": "Si us plau, obre primer una carpeta de projecte",
 		"update_support_prompt": "Ha fallat l'actualització del missatge de suport",
 		"reset_support_prompt": "Ha fallat el restabliment del missatge de suport",
@@ -111,7 +112,8 @@
 	},
 	"buttons": {
 		"save": "Desar",
-		"edit": "Editar"
+		"edit": "Editar",
+		"learn_more": "Més informació"
 	},
 	"tasks": {
 		"canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.",

+ 3 - 1
src/i18n/locales/de/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Datei konnte nicht geöffnet werden!",
 		"checkpoint_timeout": "Zeitüberschreitung beim Versuch, den Checkpoint wiederherzustellen.",
 		"checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.",
+		"git_not_installed": "Git ist für die Checkpoint-Funktion erforderlich. Bitte installiere Git, um Checkpoints zu aktivieren.",
 		"no_workspace": "Bitte öffne zuerst einen Projektordner",
 		"update_support_prompt": "Fehler beim Aktualisieren der Support-Nachricht",
 		"reset_support_prompt": "Fehler beim Zurücksetzen der Support-Nachricht",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Speichern",
-		"edit": "Bearbeiten"
+		"edit": "Bearbeiten",
+		"learn_more": "Mehr erfahren"
 	},
 	"tasks": {
 		"canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.",

+ 3 - 1
src/i18n/locales/en/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Could not open file!",
 		"checkpoint_timeout": "Timed out when attempting to restore checkpoint.",
 		"checkpoint_failed": "Failed to restore checkpoint.",
+		"git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.",
 		"no_workspace": "Please open a project folder first",
 		"update_support_prompt": "Failed to update support prompt",
 		"reset_support_prompt": "Failed to reset support prompt",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Save",
-		"edit": "Edit"
+		"edit": "Edit",
+		"learn_more": "Learn More"
 	},
 	"tasks": {
 		"canceled": "Task error: It was stopped and canceled by the user.",

+ 3 - 1
src/i18n/locales/es/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "¡No se pudo abrir el archivo!",
 		"checkpoint_timeout": "Se agotó el tiempo al intentar restaurar el punto de control.",
 		"checkpoint_failed": "Error al restaurar el punto de control.",
+		"git_not_installed": "Git es necesario para la función de puntos de control. Por favor, instala Git para activar los puntos de control.",
 		"no_workspace": "Por favor, abre primero una carpeta de proyecto",
 		"update_support_prompt": "Error al actualizar el mensaje de soporte",
 		"reset_support_prompt": "Error al restablecer el mensaje de soporte",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Guardar",
-		"edit": "Editar"
+		"edit": "Editar",
+		"learn_more": "Más información"
 	},
 	"tasks": {
 		"canceled": "Error de tarea: Fue detenida y cancelada por el usuario.",

+ 3 - 1
src/i18n/locales/fr/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Impossible d'ouvrir le fichier !",
 		"checkpoint_timeout": "Expiration du délai lors de la tentative de rétablissement du checkpoint.",
 		"checkpoint_failed": "Échec du rétablissement du checkpoint.",
+		"git_not_installed": "Git est requis pour la fonctionnalité des points de contrôle. Veuillez installer Git pour activer les points de contrôle.",
 		"no_workspace": "Veuillez d'abord ouvrir un espace de travail",
 		"update_support_prompt": "Erreur lors de la mise à jour du prompt de support",
 		"reset_support_prompt": "Erreur lors de la réinitialisation du prompt de support",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Enregistrer",
-		"edit": "Modifier"
+		"edit": "Modifier",
+		"learn_more": "En savoir plus"
 	},
 	"tasks": {
 		"canceled": "Erreur de tâche : Elle a été arrêtée et annulée par l'utilisateur.",

+ 3 - 1
src/i18n/locales/hi/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "फ़ाइल नहीं खोली जा सकी!",
 		"checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।",
 		"checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।",
+		"git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट সক্ষম करने के लिए Git इंस्टॉल करें।",
 		"no_workspace": "कृपया पहले प्रोजेक्ट फ़ोल्डर खोलें",
 		"update_support_prompt": "सपोर्ट प्रॉम्प्ट अपडेट करने में विफल",
 		"reset_support_prompt": "सपोर्ट प्रॉम्प्ट रीसेट करने में विफल",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "सहेजें",
-		"edit": "संपादित करें"
+		"edit": "संपादित करें",
+		"learn_more": "और अधिक जानें"
 	},
 	"tasks": {
 		"canceled": "टास्क त्रुटि: इसे उपयोगकर्ता द्वारा रोका और रद्द किया गया था।",

+ 3 - 1
src/i18n/locales/id/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Tidak dapat membuka file!",
 		"checkpoint_timeout": "Timeout saat mencoba memulihkan checkpoint.",
 		"checkpoint_failed": "Gagal memulihkan checkpoint.",
+		"git_not_installed": "Git diperlukan untuk fitur checkpoint. Silakan instal Git untuk mengaktifkan checkpoint.",
 		"no_workspace": "Silakan buka folder proyek terlebih dahulu",
 		"update_support_prompt": "Gagal memperbarui support prompt",
 		"reset_support_prompt": "Gagal mereset support prompt",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Simpan",
-		"edit": "Edit"
+		"edit": "Edit",
+		"learn_more": "Pelajari Lebih Lanjut"
 	},
 	"tasks": {
 		"canceled": "Error tugas: Dihentikan dan dibatalkan oleh pengguna.",

+ 3 - 1
src/i18n/locales/it/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Impossibile aprire il file!",
 		"checkpoint_timeout": "Timeout durante il tentativo di ripristinare il checkpoint.",
 		"checkpoint_failed": "Impossibile ripristinare il checkpoint.",
+		"git_not_installed": "Git è richiesto per la funzione di checkpoint. Per favore, installa Git per abilitare i checkpoint.",
 		"no_workspace": "Per favore, apri prima una cartella di progetto",
 		"update_support_prompt": "Errore durante l'aggiornamento del messaggio di supporto",
 		"reset_support_prompt": "Errore durante il ripristino del messaggio di supporto",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Salva",
-		"edit": "Modifica"
+		"edit": "Modifica",
+		"learn_more": "Scopri di più"
 	},
 	"tasks": {
 		"canceled": "Errore attività: È stata interrotta e annullata dall'utente.",

+ 3 - 1
src/i18n/locales/ja/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "ファイルを開けませんでした!",
 		"checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。",
 		"checkpoint_failed": "チェックポイントの復元に失敗しました。",
+		"git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。",
 		"no_workspace": "まずプロジェクトフォルダを開いてください",
 		"update_support_prompt": "サポートメッセージの更新に失敗しました",
 		"reset_support_prompt": "サポートメッセージのリセットに失敗しました",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "保存",
-		"edit": "編集"
+		"edit": "編集",
+		"learn_more": "詳細"
 	},
 	"tasks": {
 		"canceled": "タスクエラー:ユーザーによって停止およびキャンセルされました。",

+ 3 - 1
src/i18n/locales/ko/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "파일을 열 수 없습니다!",
 		"checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.",
 		"checkpoint_failed": "체크포인트 복원에 실패했습니다.",
+		"git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.",
 		"no_workspace": "먼저 프로젝트 폴더를 열어주세요",
 		"update_support_prompt": "지원 프롬프트 업데이트에 실패했습니다",
 		"reset_support_prompt": "지원 프롬프트 재설정에 실패했습니다",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "저장",
-		"edit": "편집"
+		"edit": "편집",
+		"learn_more": "더 알아보기"
 	},
 	"tasks": {
 		"canceled": "작업 오류: 사용자에 의해 중지 및 취소되었습니다.",

+ 3 - 1
src/i18n/locales/nl/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Kon bestand niet openen!",
 		"checkpoint_timeout": "Time-out bij het herstellen van checkpoint.",
 		"checkpoint_failed": "Herstellen van checkpoint mislukt.",
+		"git_not_installed": "Git is vereist voor de checkpoint-functie. Installeer Git om checkpoints in te schakelen.",
 		"no_workspace": "Open eerst een projectmap",
 		"update_support_prompt": "Bijwerken van ondersteuningsprompt mislukt",
 		"reset_support_prompt": "Resetten van ondersteuningsprompt mislukt",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Opslaan",
-		"edit": "Bewerken"
+		"edit": "Bewerken",
+		"learn_more": "Meer informatie"
 	},
 	"tasks": {
 		"canceled": "Taakfout: gestopt en geannuleerd door gebruiker.",

+ 3 - 1
src/i18n/locales/pl/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Nie można otworzyć pliku!",
 		"checkpoint_timeout": "Upłynął limit czasu podczas próby przywrócenia punktu kontrolnego.",
 		"checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.",
+		"git_not_installed": "Funkcja punktów kontrolnych wymaga oprogramowania Git. Zainstaluj Git, aby włączyć punkty kontrolne.",
 		"no_workspace": "Najpierw otwórz folder projektu",
 		"update_support_prompt": "Nie udało się zaktualizować komunikatu wsparcia",
 		"reset_support_prompt": "Nie udało się zresetować komunikatu wsparcia",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Zapisz",
-		"edit": "Edytuj"
+		"edit": "Edytuj",
+		"learn_more": "Dowiedz się więcej"
 	},
 	"tasks": {
 		"canceled": "Błąd zadania: Zostało zatrzymane i anulowane przez użytkownika.",

+ 3 - 1
src/i18n/locales/pt-BR/common.json

@@ -32,6 +32,7 @@
 		"could_not_open_file_generic": "Não foi possível abrir o arquivo!",
 		"checkpoint_timeout": "Tempo esgotado ao tentar restaurar o ponto de verificação.",
 		"checkpoint_failed": "Falha ao restaurar o ponto de verificação.",
+		"git_not_installed": "O Git é necessário para o recurso de checkpoints. Por favor, instale o Git para habilitar os checkpoints.",
 		"no_workspace": "Por favor, abra primeiro uma pasta de projeto",
 		"update_support_prompt": "Falha ao atualizar o prompt de suporte",
 		"reset_support_prompt": "Falha ao redefinir o prompt de suporte",
@@ -111,7 +112,8 @@
 	},
 	"buttons": {
 		"save": "Salvar",
-		"edit": "Editar"
+		"edit": "Editar",
+		"learn_more": "Saiba Mais"
 	},
 	"tasks": {
 		"canceled": "Erro na tarefa: Foi interrompida e cancelada pelo usuário.",

+ 3 - 1
src/i18n/locales/ru/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Не удалось открыть файл!",
 		"checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.",
 		"checkpoint_failed": "Не удалось восстановить контрольную точку.",
+		"git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.",
 		"no_workspace": "Пожалуйста, сначала откройте папку проекта",
 		"update_support_prompt": "Не удалось обновить промпт поддержки",
 		"reset_support_prompt": "Не удалось сбросить промпт поддержки",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Сохранить",
-		"edit": "Редактировать"
+		"edit": "Редактировать",
+		"learn_more": "Узнать больше"
 	},
 	"tasks": {
 		"canceled": "Ошибка задачи: Она была остановлена и отменена пользователем.",

+ 3 - 1
src/i18n/locales/tr/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Dosya açılamadı!",
 		"checkpoint_timeout": "Kontrol noktasını geri yüklemeye çalışırken zaman aşımına uğradı.",
 		"checkpoint_failed": "Kontrol noktası geri yüklenemedi.",
+		"git_not_installed": "Kontrol noktaları özelliği için Git gereklidir. Kontrol noktalarını etkinleştirmek için lütfen Git'i yükleyin.",
 		"no_workspace": "Lütfen önce bir proje klasörü açın",
 		"update_support_prompt": "Destek istemi güncellenemedi",
 		"reset_support_prompt": "Destek istemi sıfırlanamadı",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Kaydet",
-		"edit": "Düzenle"
+		"edit": "Düzenle",
+		"learn_more": "Daha Fazla Bilgi"
 	},
 	"tasks": {
 		"canceled": "Görev hatası: Kullanıcı tarafından durduruldu ve iptal edildi.",

+ 3 - 1
src/i18n/locales/vi/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "Không thể mở tệp!",
 		"checkpoint_timeout": "Đã hết thời gian khi cố gắng khôi phục điểm kiểm tra.",
 		"checkpoint_failed": "Không thể khôi phục điểm kiểm tra.",
+		"git_not_installed": "Yêu cầu Git cho tính năng điểm kiểm tra. Vui lòng cài đặt Git để bật điểm kiểm tra.",
 		"no_workspace": "Vui lòng mở thư mục dự án trước",
 		"update_support_prompt": "Không thể cập nhật lời nhắc hỗ trợ",
 		"reset_support_prompt": "Không thể đặt lại lời nhắc hỗ trợ",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "Lưu",
-		"edit": "Chỉnh sửa"
+		"edit": "Chỉnh sửa",
+		"learn_more": "Tìm hiểu thêm"
 	},
 	"tasks": {
 		"canceled": "Lỗi nhiệm vụ: Nó đã bị dừng và hủy bởi người dùng.",

+ 3 - 1
src/i18n/locales/zh-CN/common.json

@@ -33,6 +33,7 @@
 		"could_not_open_file_generic": "无法打开文件!",
 		"checkpoint_timeout": "尝试恢复检查点时超时。",
 		"checkpoint_failed": "恢复检查点失败。",
+		"git_not_installed": "存档点功能需要 Git。请安装 Git 以启用存档点。",
 		"no_workspace": "请先打开项目文件夹",
 		"update_support_prompt": "更新支持消息失败",
 		"reset_support_prompt": "重置支持消息失败",
@@ -112,7 +113,8 @@
 	},
 	"buttons": {
 		"save": "保存",
-		"edit": "编辑"
+		"edit": "编辑",
+		"learn_more": "了解更多"
 	},
 	"tasks": {
 		"canceled": "任务错误:它已被用户停止并取消。",

+ 3 - 1
src/i18n/locales/zh-TW/common.json

@@ -28,6 +28,7 @@
 		"could_not_open_file_generic": "無法開啟檔案!",
 		"checkpoint_timeout": "嘗試恢復檢查點時超時。",
 		"checkpoint_failed": "恢復檢查點失敗。",
+		"git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。",
 		"no_workspace": "請先開啟專案資料夾",
 		"update_support_prompt": "更新支援訊息失敗",
 		"reset_support_prompt": "重設支援訊息失敗",
@@ -107,7 +108,8 @@
 	},
 	"buttons": {
 		"save": "儲存",
-		"edit": "編輯"
+		"edit": "編輯",
+		"learn_more": "了解更多"
 	},
 	"tasks": {
 		"canceled": "工作錯誤:它已被使用者停止並取消。",

+ 49 - 0
src/utils/__tests__/git.spec.ts

@@ -4,6 +4,7 @@ import * as fs from "fs"
 import * as path from "path"
 
 import {
+	checkGitInstalled,
 	searchCommits,
 	getCommitInfo,
 	getWorkingState,
@@ -83,6 +84,54 @@ describe("git utils", () => {
 		vitest.clearAllMocks()
 	})
 
+	describe("checkGitInstalled", () => {
+		it("should return true when git --version succeeds", async () => {
+			vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => {
+				if (command === "git --version") {
+					callback(null, { stdout: "git version 2.39.2", stderr: "" })
+					return {} as any
+				}
+				callback(new Error("Unexpected command"))
+				return {} as any
+			})
+
+			const result = await checkGitInstalled()
+			expect(result).toBe(true)
+			expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function))
+		})
+
+		it("should return false when git --version fails", async () => {
+			vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => {
+				if (command === "git --version") {
+					callback(new Error("git not found"))
+					return {} as any
+				}
+				callback(new Error("Unexpected command"))
+				return {} as any
+			})
+
+			const result = await checkGitInstalled()
+			expect(result).toBe(false)
+			expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function))
+		})
+
+		it("should handle unexpected errors gracefully", async () => {
+			vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => {
+				if (command === "git --version") {
+					// Simulate an unexpected error
+					callback(new Error("Unexpected system error"))
+					return {} as any
+				}
+				callback(new Error("Unexpected command"))
+				return {} as any
+			})
+
+			const result = await checkGitInstalled()
+			expect(result).toBe(false)
+			expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function))
+		})
+	})
+
 	describe("searchCommits", () => {
 		const mockCommitData = [
 			"abc123def456",

+ 10 - 1
src/utils/git.ts

@@ -210,7 +210,16 @@ async function checkGitRepo(cwd: string): Promise<boolean> {
 	}
 }
 
-async function checkGitInstalled(): Promise<boolean> {
+/**
+ * Checks if Git is installed on the system by attempting to run git --version
+ * @returns {Promise<boolean>} True if Git is installed and accessible, false otherwise
+ * @example
+ * const isGitInstalled = await checkGitInstalled();
+ * if (!isGitInstalled) {
+ *   console.log("Git is not installed");
+ * }
+ */
+export async function checkGitInstalled(): Promise<boolean> {
 	try {
 		await execAsync("git --version")
 		return true