- OAuth认证系统(Gitea + Lua扩展) - Git自动化操作(本地/SSH远程) - 实时进度WebSocket推送 - 现代化Tab界面UI - Cobra CLI命令行(init/version/serve) - 完整构建系统(Makefile + Taskfile) - UPX压缩支持(体积减少70%)
239 lines
7.7 KiB
Go
239 lines
7.7 KiB
Go
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...)
|
||
}
|