Files
2026-05-13 16:23:58 +08:00

109 lines
3.2 KiB
Go

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)
}