package gitops import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" ) type Credentials struct { Username string Secret string } type Runner struct{} func New() *Runner { return &Runner{} } func (r *Runner) CheckAvailable() error { _, err := exec.LookPath("git") return err } func (r *Runner) Clone(ctx context.Context, remoteURL, dest string, creds Credentials) error { if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { return err } return r.run(ctx, "", creds, "clone", remoteURL, dest) } func (r *Runner) Pull(ctx context.Context, repoPath string, creds Credentials) error { return r.run(ctx, repoPath, creds, "pull", "--ff-only") } func (r *Runner) Fetch(ctx context.Context, repoPath string, creds Credentials) error { return r.run(ctx, repoPath, creds, "fetch", "origin") } func (r *Runner) CurrentCommit(repoPath string) (string, error) { return r.output(context.Background(), repoPath, Credentials{}, "rev-parse", "HEAD") } func (r *Runner) RemoteCommit(ctx context.Context, repoPath, branch string, creds Credentials) (string, error) { out, err := r.output(ctx, repoPath, creds, "ls-remote", "origin", "refs/heads/"+branch) if err != nil { return "", err } fields := strings.Fields(out) if len(fields) == 0 { return "", fmt.Errorf("remote branch %s not found", branch) } return fields[0], nil } func (r *Runner) HasLocalChanges(repoPath string) (bool, error) { out, err := r.output(context.Background(), repoPath, Credentials{}, "status", "--porcelain") if err != nil { return false, err } return strings.TrimSpace(out) != "", nil } func (r *Runner) run(ctx context.Context, dir string, creds Credentials, args ...string) error { out, err := r.command(ctx, dir, creds, args...).CombinedOutput() if err != nil { return fmt.Errorf("git %s failed: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out))) } return nil } func (r *Runner) output(ctx context.Context, dir string, creds Credentials, args ...string) (string, error) { out, err := r.command(ctx, dir, creds, args...).CombinedOutput() if err != nil { return "", fmt.Errorf("git %s failed: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out))) } return strings.TrimSpace(string(out)), nil } func (r *Runner) command(ctx context.Context, dir string, creds Credentials, args ...string) *exec.Cmd { fullArgs := append([]string{"-c", "credential.helper=", "-c", "credential.useHttpPath=true"}, args...) cmd := exec.CommandContext(ctx, "git", fullArgs...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") if creds.Username != "" || creds.Secret != "" { scriptPath := askPassScript() _ = ensureAskPassScript(scriptPath) cmd.Env = append( cmd.Env, "GIT_ASKPASS="+scriptPath, "GITEA_USERNAME="+creds.Username, "GITEA_PASSWORD="+creds.Secret, ) } return cmd } func askPassScript() string { return filepath.Join(os.TempDir(), "sgg-ai-skill-manager-git-askpass.cmd") } func ensureAskPassScript(path string) error { content := "@echo off\r\nset prompt=%~1\r\necho %prompt% | findstr /I \"username\" >nul\r\nif %ERRORLEVEL%==0 (echo %GITEA_USERNAME%) else (echo %GITEA_PASSWORD%)\r\n" return os.WriteFile(path, []byte(content), 0o600) }