186 lines
4.8 KiB
Go
186 lines
4.8 KiB
Go
package git
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os/exec"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||
"github.com/gitcodestatic/gitcodestatic/internal/models"
|
||
)
|
||
|
||
// CmdGitManager 基于git命令的实现
|
||
type CmdGitManager struct {
|
||
gitPath string
|
||
}
|
||
|
||
// NewCmdGitManager 创建命令行Git管理器
|
||
func NewCmdGitManager(gitPath string) *CmdGitManager {
|
||
if gitPath == "" {
|
||
gitPath = "git"
|
||
}
|
||
return &CmdGitManager{gitPath: gitPath}
|
||
}
|
||
|
||
// IsAvailable 检查git命令是否可用
|
||
func (m *CmdGitManager) IsAvailable() bool {
|
||
cmd := exec.Command(m.gitPath, "--version")
|
||
err := cmd.Run()
|
||
return err == nil
|
||
}
|
||
|
||
// Clone 克隆仓库
|
||
func (m *CmdGitManager) Clone(ctx context.Context, url, localPath string, cred *models.Credential) error {
|
||
// 注入凭据到URL(如果有)
|
||
cloneURL := url
|
||
if cred != nil {
|
||
cloneURL = m.injectCredentials(url, cred)
|
||
}
|
||
|
||
cmd := exec.CommandContext(ctx, m.gitPath, "clone", cloneURL, localPath)
|
||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // 禁止交互式提示
|
||
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
// 脱敏日志
|
||
sanitizedURL := sanitizeURL(url)
|
||
logger.Logger.Error().
|
||
Err(err).
|
||
Str("url", sanitizedURL).
|
||
Str("output", string(output)).
|
||
Msg("failed to clone repository")
|
||
return fmt.Errorf("failed to clone repository: %w", err)
|
||
}
|
||
|
||
logger.Logger.Info().
|
||
Str("url", sanitizeURL(url)).
|
||
Str("local_path", localPath).
|
||
Msg("repository cloned successfully")
|
||
|
||
return nil
|
||
}
|
||
|
||
// Pull 拉取更新
|
||
func (m *CmdGitManager) Pull(ctx context.Context, localPath string, cred *models.Credential) error {
|
||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "pull")
|
||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0")
|
||
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
logger.Logger.Error().
|
||
Err(err).
|
||
Str("local_path", localPath).
|
||
Str("output", string(output)).
|
||
Msg("failed to pull repository")
|
||
return fmt.Errorf("failed to pull repository: %w", err)
|
||
}
|
||
|
||
logger.Logger.Info().
|
||
Str("local_path", localPath).
|
||
Msg("repository pulled successfully")
|
||
|
||
return nil
|
||
}
|
||
|
||
// Checkout 切换分支
|
||
func (m *CmdGitManager) Checkout(ctx context.Context, localPath, branch string) error {
|
||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "checkout", branch)
|
||
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
logger.Logger.Error().
|
||
Err(err).
|
||
Str("local_path", localPath).
|
||
Str("branch", branch).
|
||
Str("output", string(output)).
|
||
Msg("failed to checkout branch")
|
||
return fmt.Errorf("failed to checkout branch: %w", err)
|
||
}
|
||
|
||
logger.Logger.Info().
|
||
Str("local_path", localPath).
|
||
Str("branch", branch).
|
||
Msg("branch checked out successfully")
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetCurrentBranch 获取当前分支
|
||
func (m *CmdGitManager) GetCurrentBranch(ctx context.Context, localPath string) (string, error) {
|
||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "--abbrev-ref", "HEAD")
|
||
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to get current branch: %w", err)
|
||
}
|
||
|
||
branch := strings.TrimSpace(string(output))
|
||
return branch, nil
|
||
}
|
||
|
||
// GetHeadCommitHash 获取HEAD commit hash
|
||
func (m *CmdGitManager) GetHeadCommitHash(ctx context.Context, localPath string) (string, error) {
|
||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "HEAD")
|
||
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to get HEAD commit hash: %w", err)
|
||
}
|
||
|
||
hash := strings.TrimSpace(string(output))
|
||
return hash, nil
|
||
}
|
||
|
||
// CountCommits 统计提交次数
|
||
func (m *CmdGitManager) CountCommits(ctx context.Context, localPath, branch, fromDate string) (int, error) {
|
||
args := []string{"-C", localPath, "rev-list", "--count"}
|
||
|
||
if fromDate != "" {
|
||
args = append(args, "--since="+fromDate)
|
||
}
|
||
|
||
args = append(args, branch)
|
||
|
||
cmd := exec.CommandContext(ctx, m.gitPath, args...)
|
||
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to count commits: %w", err)
|
||
}
|
||
|
||
countStr := strings.TrimSpace(string(output))
|
||
count, err := strconv.Atoi(countStr)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to parse commit count: %w", err)
|
||
}
|
||
|
||
return count, nil
|
||
}
|
||
|
||
// injectCredentials 注入凭据到URL
|
||
func (m *CmdGitManager) injectCredentials(url string, cred *models.Credential) string {
|
||
if cred == nil || cred.Username == "" {
|
||
return url
|
||
}
|
||
|
||
// 简单的URL凭据注入(仅支持https)
|
||
if strings.HasPrefix(url, "https://") {
|
||
credentials := cred.Username
|
||
if cred.Password != "" {
|
||
credentials += ":" + cred.Password
|
||
}
|
||
return strings.Replace(url, "https://", "https://"+credentials+"@", 1)
|
||
}
|
||
|
||
return url
|
||
}
|
||
|
||
// sanitizeURL 脱敏URL(移除用户名密码)
|
||
func sanitizeURL(url string) string {
|
||
re := regexp.MustCompile(`(https?://)[^@]+@`)
|
||
return re.ReplaceAllString(url, "${1}***@")
|
||
}
|