diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
index 4372fef..e13d36d 100644
--- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
+++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
@@ -86,6 +86,16 @@ export function BatchPush(paths) {
}));
}
+/**
+ * CheckGitInstalled 检查 Git 是否已安装
+ * @returns {$CancellablePromise<$models.GitStatus>}
+ */
+export function CheckGitInstalled() {
+ return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
+ return $$createType2($result);
+ }));
+}
+
/**
* CheckoutRemoteBranch 检出远程分支到本地
* @param {string} path
@@ -196,7 +206,7 @@ export function FetchProject(path) {
*/
export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType3($result);
+ return $$createType4($result);
}));
}
@@ -206,7 +216,7 @@ export function GetAllProjectOverview() {
*/
export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType5($result);
+ return $$createType6($result);
}));
}
@@ -217,7 +227,7 @@ export function GetAppSettings() {
*/
export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType7($result);
+ return $$createType8($result);
}));
}
@@ -250,7 +260,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/
export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType9($result);
+ return $$createType10($result);
}));
}
@@ -262,7 +272,7 @@ export function GetCommitFiles(path, hash) {
*/
export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType11($result);
+ return $$createType12($result);
}));
}
@@ -283,7 +293,7 @@ export function GetConflictFileContent(projectPath, filePath) {
*/
export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType13($result);
+ return $$createType14($result);
}));
}
@@ -323,7 +333,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
*/
export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType15($result);
+ return $$createType16($result);
}));
}
@@ -334,7 +344,7 @@ export function GetGitGlobalConfig() {
*/
export function GetPlatformInfo(name) {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType17($result);
+ return $$createType18($result);
}));
}
@@ -345,7 +355,7 @@ export function GetPlatformInfo(name) {
*/
export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType19($result);
+ return $$createType20($result);
}));
}
@@ -356,7 +366,7 @@ export function GetProjectChangedFiles(path) {
*/
export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType21($result);
+ return $$createType22($result);
}));
}
@@ -366,7 +376,7 @@ export function GetProjectStatus(path) {
*/
export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType23($result);
+ return $$createType24($result);
}));
}
@@ -377,7 +387,7 @@ export function GetProjectTree() {
*/
export function GetRemoteBranches(path) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType7($result);
+ return $$createType8($result);
}));
}
@@ -388,7 +398,7 @@ export function GetRemoteBranches(path) {
*/
export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType25($result);
+ return $$createType26($result);
}));
}
@@ -399,7 +409,7 @@ export function GetStashList(path) {
*/
export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType27($result);
+ return $$createType28($result);
}));
}
@@ -411,10 +421,20 @@ export function GetTags(path) {
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType29($result);
+ return $$createType30($result);
}));
}
+/**
+ * InstallGit 下载并安装 Git
+ * installDir: 用户选择的安装目录(Windows 有效)
+ * @param {string} installDir
+ * @returns {$CancellablePromise
}
+ */
+export function InstallGit(installDir) {
+ return $Call.ByID(1052641569, installDir);
+}
+
/**
* IsMerging 检查是否处于合并状态
* @param {string} path
@@ -545,7 +565,7 @@ export function SaveConflictFile(projectPath, filePath, content) {
*/
export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
- return $$createType11($result);
+ return $$createType12($result);
}));
}
@@ -557,6 +577,14 @@ export function SelectDirectory() {
return $Call.ByID(2318416763);
}
+/**
+ * SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
+ * @returns {$CancellablePromise}
+ */
+export function SelectGitInstallDir() {
+ return $Call.ByID(2941074074);
+}
+
/**
* @param {application$0.App | null} app
* @returns {$CancellablePromise}
@@ -697,31 +725,32 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
// Private type creation functions
const $$createType0 = $models.BatchPullResult.createFrom;
const $$createType1 = $Create.Array($$createType0);
-const $$createType2 = $models.ProjectOverview.createFrom;
-const $$createType3 = $Create.Array($$createType2);
-const $$createType4 = config$0.Settings.createFrom;
-const $$createType5 = $Create.Nullable($$createType4);
-const $$createType6 = $models.BranchInfo.createFrom;
-const $$createType7 = $Create.Array($$createType6);
-const $$createType8 = $models.CommitFileInfo.createFrom;
-const $$createType9 = $Create.Array($$createType8);
-const $$createType10 = $models.CommitLog.createFrom;
-const $$createType11 = $Create.Array($$createType10);
-const $$createType12 = $models.ConflictFileInfo.createFrom;
-const $$createType13 = $Create.Array($$createType12);
-const $$createType14 = $models.GitConfig.createFrom;
-const $$createType15 = $Create.Nullable($$createType14);
-const $$createType16 = $models.PlatformInfo.createFrom;
-const $$createType17 = $Create.Nullable($$createType16);
-const $$createType18 = $models.FileInfo.createFrom;
-const $$createType19 = $Create.Array($$createType18);
-const $$createType20 = $models.ProjectStatus.createFrom;
-const $$createType21 = $Create.Nullable($$createType20);
-const $$createType22 = $models.TreeNode.createFrom;
-const $$createType23 = $Create.Array($$createType22);
-const $$createType24 = $models.StashInfo.createFrom;
-const $$createType25 = $Create.Array($$createType24);
-const $$createType26 = $models.TagInfo.createFrom;
-const $$createType27 = $Create.Array($$createType26);
-const $$createType28 = $models.UserInfo.createFrom;
-const $$createType29 = $Create.Nullable($$createType28);
+const $$createType2 = $models.GitStatus.createFrom;
+const $$createType3 = $models.ProjectOverview.createFrom;
+const $$createType4 = $Create.Array($$createType3);
+const $$createType5 = config$0.Settings.createFrom;
+const $$createType6 = $Create.Nullable($$createType5);
+const $$createType7 = $models.BranchInfo.createFrom;
+const $$createType8 = $Create.Array($$createType7);
+const $$createType9 = $models.CommitFileInfo.createFrom;
+const $$createType10 = $Create.Array($$createType9);
+const $$createType11 = $models.CommitLog.createFrom;
+const $$createType12 = $Create.Array($$createType11);
+const $$createType13 = $models.ConflictFileInfo.createFrom;
+const $$createType14 = $Create.Array($$createType13);
+const $$createType15 = $models.GitConfig.createFrom;
+const $$createType16 = $Create.Nullable($$createType15);
+const $$createType17 = $models.PlatformInfo.createFrom;
+const $$createType18 = $Create.Nullable($$createType17);
+const $$createType19 = $models.FileInfo.createFrom;
+const $$createType20 = $Create.Array($$createType19);
+const $$createType21 = $models.ProjectStatus.createFrom;
+const $$createType22 = $Create.Nullable($$createType21);
+const $$createType23 = $models.TreeNode.createFrom;
+const $$createType24 = $Create.Array($$createType23);
+const $$createType25 = $models.StashInfo.createFrom;
+const $$createType26 = $Create.Array($$createType25);
+const $$createType27 = $models.TagInfo.createFrom;
+const $$createType28 = $Create.Array($$createType27);
+const $$createType29 = $models.UserInfo.createFrom;
+const $$createType30 = $Create.Nullable($$createType29);
diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js
index 8f4cabe..72f781d 100644
--- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js
+++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js
@@ -15,6 +15,7 @@ export {
ConflictFileInfo,
FileInfo,
GitConfig,
+ GitStatus,
PlatformInfo,
ProjectOverview,
ProjectStatus,
diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
index 636b6ec..a20406d 100644
--- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
+++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
@@ -328,6 +328,51 @@ export class GitConfig {
}
}
+/**
+ * GitStatus Git 安装状态
+ */
+export class GitStatus {
+ /**
+ * Creates a new GitStatus instance.
+ * @param {Partial} [$$source = {}] - The source object to create the GitStatus.
+ */
+ constructor($$source = {}) {
+ if (!("installed" in $$source)) {
+ /**
+ * @member
+ * @type {boolean}
+ */
+ this["installed"] = false;
+ }
+ if (!("version" in $$source)) {
+ /**
+ * @member
+ * @type {string}
+ */
+ this["version"] = "";
+ }
+ if (!("path" in $$source)) {
+ /**
+ * @member
+ * @type {string}
+ */
+ this["path"] = "";
+ }
+
+ Object.assign(this, $$source);
+ }
+
+ /**
+ * Creates a new GitStatus instance from a string or object.
+ * @param {any} [$$source = {}]
+ * @returns {GitStatus}
+ */
+ static createFrom($$source = {}) {
+ let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
+ return new GitStatus(/** @type {Partial} */($$parsedSource));
+ }
+}
+
/**
* PlatformInfo 平台信息
*/
diff --git a/internal/app/gitsetup.go b/internal/app/gitsetup.go
new file mode 100644
index 0000000..cf989ae
--- /dev/null
+++ b/internal/app/gitsetup.go
@@ -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
+}
diff --git a/internal/app/hidewindow_other.go b/internal/app/hidewindow_other.go
new file mode 100644
index 0000000..649eeef
--- /dev/null
+++ b/internal/app/hidewindow_other.go
@@ -0,0 +1,8 @@
+//go:build !windows
+
+package app
+
+import "os/exec"
+
+// hideWindowCmd 非 Windows 平台无需处理
+func hideWindowCmd(cmd *exec.Cmd) {}
diff --git a/internal/app/hidewindow_windows.go b/internal/app/hidewindow_windows.go
new file mode 100644
index 0000000..a5d4ef5
--- /dev/null
+++ b/internal/app/hidewindow_windows.go
@@ -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,
+ }
+}