package git import ( "cs-bridge/pkg/logger" "fmt" "os" "os/exec" "path" "path/filepath" "strings" ) // ProgressCallback 进度回调函数类型 // 用于向外部报告git操作的进度 type ProgressCallback func(message string, percent int) // SSHConfig SSH连接配置 type SSHConfig struct { Host string // SSH服务器地址 Port int // SSH端口 User string // SSH用户名 KeyPath string // SSH私钥路径 } // CheckRepoExists 检查指定路径是否存在git仓库 // path: 要检查的路径 // 返回true表示存在.git目录,false表示不存在 func CheckRepoExists(path string) bool { log := logger.GetLogger() gitDir := filepath.Join(path, ".git") info, err := os.Stat(gitDir) if err != nil { log.Debug(fmt.Sprintf("[Git] 本地仓库不存在: %s", path)) return false } exists := info.IsDir() log.Info(fmt.Sprintf("[Git] 本地仓库检查 - Path: %s, Exists: %v", path, exists)) return exists } // CheckRepoExistsRemote 通过SSH检查远程服务器上是否存在git仓库 func CheckRepoExistsRemote(sshCfg SSHConfig, path string) bool { log := logger.GetLogger() log.Info(fmt.Sprintf("[Git] SSH检查远程仓库 - Host: %s, Path: %s", sshCfg.Host, path)) cmd := buildSSHCommand(sshCfg, fmt.Sprintf("test -d '%s/.git' && echo 'exists' || echo 'not_exists'", path)) output, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Sprintf("[Git] SSH检查失败: %v, Output: %s", err, string(output))) return false } exists := strings.TrimSpace(string(output)) == "exists" log.Info(fmt.Sprintf("[Git] SSH远程仓库检查结果 - Exists: %v", exists)) return exists } // CloneRepo 克隆git仓库(本地执行) func CloneRepo(repoURL, destPath string, callback ProgressCallback) error { log := logger.GetLogger() log.Info(fmt.Sprintf("[Git] 开始本地克隆 - URL: %s, Dest: %s", repoURL, destPath)) // 确保父目录存在 parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { log.Error(fmt.Sprintf("[Git] 创建父目录失败: %v", err)) return fmt.Errorf("failed to create parent directory: %w", err) } // 如果目标目录存在但不是git仓库,删除它 if _, err := os.Stat(destPath); err == nil { if !CheckRepoExists(destPath) { log.Info(fmt.Sprintf("[Git] 清理非git目录: %s", destPath)) if err := os.RemoveAll(destPath); err != nil { log.Error(fmt.Sprintf("[Git] 清理目录失败: %v", err)) return fmt.Errorf("failed to clean destination directory: %w", err) } } } if callback != nil { callback("正在克隆仓库...", 10) } // 执行git clone命令 cmd := exec.Command("git", "clone", repoURL, destPath) output, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Sprintf("[Git] 克隆失败: %v, Output: %s", err, string(output))) return fmt.Errorf("git clone failed: %w, output: %s", err, string(output)) } log.Info(fmt.Sprintf("[Git] 克隆成功: %s", destPath)) if callback != nil { callback("仓库克隆完成", 100) } return nil } // CloneRepoRemote 通过SSH在远程服务器上克隆git仓库 func CloneRepoRemote(sshCfg SSHConfig, repoURL, destPath string, callback ProgressCallback) error { log := logger.GetLogger() log.Info(fmt.Sprintf("[Git] 开始SSH远程克隆 - Host: %s, URL: %s, Dest: %s", sshCfg.Host, repoURL, destPath)) if callback != nil { callback("正在通过SSH连接服务器...", 5) } // 确保父目录存在 parentDir := path.Dir(destPath) log.Debug(fmt.Sprintf("[Git] 创建远程父目录: %s", parentDir)) mkdirCmd := buildSSHCommand(sshCfg, fmt.Sprintf("mkdir -p '%s'", parentDir)) if output, err := mkdirCmd.CombinedOutput(); err != nil { log.Error(fmt.Sprintf("[Git] SSH创建目录失败: %v, Output: %s", err, string(output))) return fmt.Errorf("failed to create parent directory: %w, output: %s", err, string(output)) } // 如果目标目录存在但不是git仓库,删除它 log.Debug(fmt.Sprintf("[Git] 检查并清理远程目录: %s", destPath)) cleanCmd := buildSSHCommand(sshCfg, fmt.Sprintf( "if [ -d '%s' ] && [ ! -d '%s/.git' ]; then rm -rf '%s'; fi", destPath, destPath, destPath, )) if output, err := cleanCmd.CombinedOutput(); err != nil { log.Error(fmt.Sprintf("[Git] SSH清理目录失败: %v, Output: %s", err, string(output))) return fmt.Errorf("failed to clean destination: %w, output: %s", err, string(output)) } if callback != nil { callback("正在克隆仓库...", 20) } // 执行git clone log.Info(fmt.Sprintf("[Git] 执行SSH git clone: %s -> %s", repoURL, destPath)) cloneCmd := buildSSHCommand(sshCfg, fmt.Sprintf("git clone '%s' '%s'", repoURL, destPath)) output, err := cloneCmd.CombinedOutput() if err != nil { log.Error(fmt.Sprintf("[Git] SSH克隆失败: %v, Output: %s", err, string(output))) return fmt.Errorf("git clone failed: %w, output: %s", err, string(output)) } log.Info(fmt.Sprintf("[Git] SSH克隆成功: %s", destPath)) if callback != nil { callback("仓库克隆完成", 100) } return nil } // PullRepo 更新git仓库(本地执行) func PullRepo(repoPath string, callback ProgressCallback) error { log := logger.GetLogger() log.Info(fmt.Sprintf("[Git] 开始本地更新仓库: %s", repoPath)) if !CheckRepoExists(repoPath) { return fmt.Errorf("not a git repository: %s", repoPath) } if callback != nil { callback("正在更新仓库...", 10) } cmd := exec.Command("git", "pull") cmd.Dir = repoPath output, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Sprintf("[Git] 更新失败: %v, Output: %s", err, string(output))) return fmt.Errorf("git pull failed: %w, output: %s", err, string(output)) } log.Info(fmt.Sprintf("[Git] 更新成功: %s", strings.TrimSpace(string(output)))) if callback != nil { callback(fmt.Sprintf("仓库更新完成: %s", strings.TrimSpace(string(output))), 100) } return nil } // PullRepoRemote 通过SSH在远程服务器上更新git仓库 func PullRepoRemote(sshCfg SSHConfig, repoPath string, callback ProgressCallback) error { log := logger.GetLogger() log.Info(fmt.Sprintf("[Git] 开始SSH远程更新仓库 - Host: %s, Path: %s", sshCfg.Host, repoPath)) if callback != nil { callback("正在通过SSH连接服务器...", 5) } if callback != nil { callback("正在更新仓库...", 20) } pullCmd := buildSSHCommand(sshCfg, fmt.Sprintf("cd '%s' && git pull", repoPath)) output, err := pullCmd.CombinedOutput() if err != nil { log.Error(fmt.Sprintf("[Git] SSH更新失败: %v, Output: %s", err, string(output))) return fmt.Errorf("git pull failed: %w, output: %s", err, string(output)) } log.Info(fmt.Sprintf("[Git] SSH更新成功: %s", strings.TrimSpace(string(output)))) if callback != nil { callback(fmt.Sprintf("仓库更新完成: %s", strings.TrimSpace(string(output))), 100) } return nil } // GetRepoName 从仓库URL中提取仓库名称 func GetRepoName(repoURL string) string { base := filepath.Base(repoURL) if len(base) > 4 && base[len(base)-4:] == ".git" { return base[:len(base)-4] } return base } // buildSSHCommand 构建SSH命令 func buildSSHCommand(sshCfg SSHConfig, remoteCmd string) *exec.Cmd { log := logger.GetLogger() // SSH密钥路径需要转换为Unix风格(SSH命令总是在Linux上执行) // 即使在Windows上编译,SSH密钥路径传给ssh命令时也必须用正斜杠 keyPath := filepath.ToSlash(sshCfg.KeyPath) args := []string{ "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", } if sshCfg.Port != 0 && sshCfg.Port != 22 { args = append(args, "-p", fmt.Sprintf("%d", sshCfg.Port)) } if keyPath != "" { // Use the converted keyPath args = append(args, "-i", keyPath) } args = append(args, fmt.Sprintf("%s@%s", sshCfg.User, sshCfg.Host), remoteCmd) log.Debug(fmt.Sprintf("[Git] SSH命令: ssh %s", strings.Join(args, " "))) return exec.Command("ssh", args...) }