package main import ( "context" "fmt" "os/exec" "strings" "time" ) const gitTimeoutSeconds = 120 // gitRun ejecuta git con argumentos en cwd. Devuelve stdout, stderr, exitCode. func gitRun(cwd string, args ...string) (string, string, int, error) { if _, err := exec.LookPath("git"); err != nil { return "", "", -1, fmt.Errorf("git binary not in PATH") } ctx, cancel := context.WithTimeout(context.Background(), gitTimeoutSeconds*time.Second) defer cancel() c := exec.CommandContext(ctx, "git", args...) // #nosec G204 — args controlled if cwd != "" { c.Dir = cwd } var stdout, stderr strings.Builder c.Stdout = &stdout c.Stderr = &stderr err := c.Run() exitCode := 0 if err != nil { if ee, ok := err.(*exec.ExitError); ok { exitCode = ee.ExitCode() } else { return stdout.String(), stderr.String(), -1, err } } return stdout.String(), stderr.String(), exitCode, nil } // runGitClone clona un repo a dest. Valida dest contra cap.PathsAllowed. func runGitClone(cap *Capability, args map[string]any) (any, int, error) { url := mapStringField(args, "url") dest := mapStringField(args, "dest") if url == "" || dest == "" { return nil, -1, fmt.Errorf("url and dest required") } if !isPathAllowed(dest, cap.PathsAllowed) { return nil, -1, fmt.Errorf("dest not allowed by manifest: %s", dest) } stdout, stderr, code, err := gitRun("", "clone", url, dest) if err != nil { return nil, -1, fmt.Errorf("git clone: %w (%s)", err, stderr) } if code != 0 { return map[string]any{ "stdout": stdout, "stderr": stderr, "exit_code": code, }, code, fmt.Errorf("git clone exit=%d: %s", code, stderr) } // recoge HEAD commit + branch sha, _, _, _ := gitRun(dest, "rev-parse", "HEAD") branch, _, _, _ := gitRun(dest, "rev-parse", "--abbrev-ref", "HEAD") return map[string]any{ "dest": dest, "commit_sha": strings.TrimSpace(sha), "branch": strings.TrimSpace(branch), "stdout": stdout, "exit_code": 0, }, 0, nil } // runGitCommit hace git add + commit en repo. files opcional; vacio = -am. func runGitCommit(cap *Capability, args map[string]any) (any, int, error) { repo := mapStringField(args, "repo") msg := mapStringField(args, "message") if repo == "" || msg == "" { return nil, -1, fmt.Errorf("repo and message required") } if !isPathAllowed(repo, cap.PathsAllowed) { return nil, -1, fmt.Errorf("repo not allowed by manifest: %s", repo) } var files []string if raw, ok := args["files"]; ok && raw != nil { if arr, ok := raw.([]any); ok { for _, v := range arr { if s, ok := v.(string); ok { files = append(files, s) } } } } if len(files) > 0 { addArgs := append([]string{"add", "--"}, files...) _, addStderr, code, err := gitRun(repo, addArgs...) if err != nil || code != 0 { return nil, code, fmt.Errorf("git add: %s", addStderr) } _, cStderr, code, err := gitRun(repo, "commit", "-m", msg) if err != nil || code != 0 { return map[string]any{"stderr": cStderr, "exit_code": code}, code, fmt.Errorf("git commit exit=%d: %s", code, cStderr) } } else { _, cStderr, code, err := gitRun(repo, "commit", "-am", msg) if err != nil || code != 0 { return map[string]any{"stderr": cStderr, "exit_code": code}, code, fmt.Errorf("git commit exit=%d: %s", code, cStderr) } } sha, _, _, _ := gitRun(repo, "rev-parse", "HEAD") return map[string]any{ "repo": repo, "commit_sha": strings.TrimSpace(sha), "exit_code": 0, }, 0, nil } // runGitPush push del repo. remote default "origin", branch default "HEAD". func runGitPush(cap *Capability, args map[string]any) (any, int, error) { repo := mapStringField(args, "repo") if repo == "" { return nil, -1, fmt.Errorf("repo required") } if !isPathAllowed(repo, cap.PathsAllowed) { return nil, -1, fmt.Errorf("repo not allowed by manifest: %s", repo) } remote := mapStringField(args, "remote") if remote == "" { remote = "origin" } branch := mapStringField(args, "branch") if branch == "" { branch = "HEAD" } stdout, stderr, code, err := gitRun(repo, "push", remote, branch) if err != nil { return nil, -1, fmt.Errorf("git push: %w (%s)", err, stderr) } if code != 0 { return map[string]any{ "stdout": stdout, "stderr": stderr, "exit_code": code, }, code, fmt.Errorf("git push exit=%d: %s", code, stderr) } return map[string]any{ "ok": true, "remote": remote, "branch": branch, "stdout": stdout, "stderr": stderr, "exit_code": 0, }, 0, nil }