Files
transfer-site/pkg/git/git.go
zhilv 8265df0dcd feat: 初始提交 - Code Server Bridge完整实现
- OAuth认证系统(Gitea + Lua扩展)
- Git自动化操作(本地/SSH远程)
- 实时进度WebSocket推送
- 现代化Tab界面UI
- Cobra CLI命令行(init/version/serve)
- 完整构建系统(Makefile + Taskfile)
- UPX压缩支持(体积减少70%)
2026-01-08 23:32:29 +08:00

239 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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...)
}