153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
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
|
|
}
|