feat: 启动时检测 Git 安装状态,支持自动下载安装\n\n- 启动时自动检测 Git 是否已安装\n- 未安装时显示全屏提示界面\n- 支持用户选择安装路径\n- 自动从 GitHub 下载 Git for Windows 并静默安装\n- 实时显示下载进度和安装状态\n- 安装完成后自动重新初始化 Git 客户端"

This commit is contained in:
zyj
2026-03-11 10:18:26 +08:00
parent 5154bb5564
commit beb40688ce
8 changed files with 768 additions and 45 deletions

241
internal/app/gitsetup.go Normal file
View File

@@ -0,0 +1,241 @@
package app
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhuy1228/GitPilot/internal/git"
)
// GitStatus Git 安装状态
type GitStatus struct {
Installed bool `json:"installed"`
Version string `json:"version"`
Path string `json:"path"`
}
// GitInstallProgress 安装进度
type GitInstallProgress struct {
Phase string `json:"phase"` // downloading, installing, done, error
Percent float64 `json:"percent"` // 下载百分比 0-100
Message string `json:"message"`
}
// CheckGitInstalled 检查 Git 是否已安装
func (s *AppService) CheckGitInstalled() GitStatus {
path, err := exec.LookPath("git")
if err != nil {
log.Println("Git 未安装:", err)
return GitStatus{Installed: false}
}
// 获取版本号
cmd := exec.Command("git", "--version")
hideWindowCmd(cmd)
output, err := cmd.Output()
if err != nil {
log.Println("获取 Git 版本失败:", err)
return GitStatus{Installed: true, Path: path}
}
version := strings.TrimSpace(string(output))
// "git version 2.43.0.windows.1" -> "2.43.0"
version = strings.TrimPrefix(version, "git version ")
if idx := strings.Index(version, ".windows"); idx > 0 {
version = version[:idx]
}
return GitStatus{
Installed: true,
Version: version,
Path: path,
}
}
// SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
func (s *AppService) SelectGitInstallDir() (string, error) {
if s.app == nil {
return "", fmt.Errorf("应用未初始化")
}
path, err := s.app.Dialog.OpenFile().
CanChooseDirectories(true).
CanChooseFiles(false).
SetTitle("选择 Git 安装路径").
PromptForSingleSelection()
if err != nil {
return "", fmt.Errorf("打开文件夹选择器失败: %w", err)
}
return path, nil
}
// getGitDownloadURL 根据平台返回 Git 下载地址
func getGitDownloadURL() string {
switch runtime.GOOS {
case "windows":
if runtime.GOARCH == "arm64" {
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-arm64.exe"
}
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.exe"
case "darwin":
return "" // macOS 建议使用 brew 或 Xcode CLI
default:
return "" // Linux 建议使用包管理器
}
}
// InstallGit 下载并安装 Git
// installDir: 用户选择的安装目录Windows 有效)
func (s *AppService) InstallGit(installDir string) error {
if runtime.GOOS != "windows" {
return fmt.Errorf("自动安装仅支持 Windows请使用系统包管理器安装 Git")
}
url := getGitDownloadURL()
if url == "" {
return fmt.Errorf("无法获取 Git 下载地址")
}
// 1. 下载安装包
s.emitInstallProgress("downloading", 0, "正在下载 Git 安装包...")
log.Println("开始下载 Git:", url)
tmpDir := os.TempDir()
installerPath := filepath.Join(tmpDir, "Git-Installer.exe")
err := s.downloadFile(url, installerPath)
if err != nil {
s.emitInstallProgress("error", 0, "下载失败: "+err.Error())
return fmt.Errorf("下载 Git 安装包失败: %w", err)
}
defer os.Remove(installerPath)
// 2. 静默安装
s.emitInstallProgress("installing", 100, "正在安装 Git请稍候...")
log.Println("开始安装 Git 到:", installDir)
// Git for Windows 静默安装参数
args := []string{
"/VERYSILENT",
"/NORESTART",
"/NOCANCEL",
"/SP-",
"/CLOSEAPPLICATIONS",
"/RESTARTAPPLICATIONS",
"/COMPONENTS=icons,ext,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh,autoupdate",
}
if installDir != "" {
args = append(args, "/DIR="+installDir)
}
cmd := exec.Command(installerPath, args...)
hideWindowCmd(cmd)
err = cmd.Run()
if err != nil {
s.emitInstallProgress("error", 0, "安装失败: "+err.Error())
return fmt.Errorf("安装 Git 失败: %w", err)
}
// 3. 验证安装
time.Sleep(2 * time.Second) // 等待 PATH 更新
status := s.CheckGitInstalled()
if !status.Installed {
// 尝试在指定目录查找
gitExe := filepath.Join(installDir, "bin", "git.exe")
if _, err := os.Stat(gitExe); err == nil {
s.emitInstallProgress("done", 100, "Git 安装成功!路径: "+gitExe)
// 重新初始化 gitClient
s.reinitGitClient()
return nil
}
s.emitInstallProgress("error", 0, "安装完成但未检测到 Git请重启应用重试")
return fmt.Errorf("安装完成但未检测到 Git")
}
s.emitInstallProgress("done", 100, "Git 安装成功!版本: "+status.Version)
s.reinitGitClient()
return nil
}
// reinitGitClient 重新初始化 Git 客户端
func (s *AppService) reinitGitClient() {
s.gitClient = git.NewGitClient()
}
// emitInstallProgress 向前端发送安装进度事件
func (s *AppService) emitInstallProgress(phase string, percent float64, message string) {
if s.app != nil {
s.app.Event.Emit("git-install-progress", GitInstallProgress{
Phase: phase,
Percent: percent,
Message: message,
})
}
}
// downloadFile 下载文件并通过事件报告进度
func (s *AppService) downloadFile(url, dest string) error {
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
totalSize := resp.ContentLength
var downloaded int64
buf := make([]byte, 32*1024)
lastReport := time.Now()
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
_, writeErr := out.Write(buf[:n])
if writeErr != nil {
return writeErr
}
downloaded += int64(n)
// 每 300ms 报告一次进度
if time.Since(lastReport) > 300*time.Millisecond {
var pct float64
if totalSize > 0 {
pct = float64(downloaded) / float64(totalSize) * 100
}
sizeMB := float64(downloaded) / 1024 / 1024
msg := fmt.Sprintf("正在下载... %.1f MB", sizeMB)
if totalSize > 0 {
totalMB := float64(totalSize) / 1024 / 1024
msg = fmt.Sprintf("正在下载... %.1f / %.1f MB", sizeMB, totalMB)
}
s.emitInstallProgress("downloading", pct, msg)
lastReport = time.Now()
}
}
if readErr != nil {
if readErr == io.EOF {
break
}
return readErr
}
}
return nil
}

View File

@@ -0,0 +1,8 @@
//go:build !windows
package app
import "os/exec"
// hideWindowCmd 非 Windows 平台无需处理
func hideWindowCmd(cmd *exec.Cmd) {}

View File

@@ -0,0 +1,14 @@
package app
import (
"os/exec"
"syscall"
)
// hideWindowCmd 在 Windows 上隐藏子进程的控制台窗口
func hideWindowCmd(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
}