Replace raw fmt/log calls with structured slog logger (Go) and console-based logger (TypeScript). Add request logging middleware. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
4.3 KiB
Go
139 lines
4.3 KiB
Go
package execenv
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// detectGitRepo checks if dir is inside a git repository.
|
|
// Returns the git root path and true if found.
|
|
func detectGitRepo(dir string) (string, bool) {
|
|
cmd := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return strings.TrimSpace(string(out)), true
|
|
}
|
|
|
|
// getDefaultBranch returns the current branch name of the git repo, falling back to HEAD.
|
|
func getDefaultBranch(gitRoot string) string {
|
|
cmd := exec.Command("git", "-C", gitRoot, "symbolic-ref", "--short", "HEAD")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "HEAD"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// setupGitWorktree creates a git worktree at worktreePath with a new branch.
|
|
func setupGitWorktree(gitRoot, worktreePath, branchName, baseRef string) error {
|
|
// Remove the workdir created by Prepare — git worktree add needs to create it.
|
|
if err := os.Remove(worktreePath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove placeholder workdir: %w", err)
|
|
}
|
|
|
|
err := runGitWorktreeAdd(gitRoot, worktreePath, branchName, baseRef)
|
|
if err != nil && strings.Contains(err.Error(), "already exists") {
|
|
// Branch name collision: append timestamp and retry once.
|
|
branchName = fmt.Sprintf("%s-%d", branchName, time.Now().Unix())
|
|
err = runGitWorktreeAdd(gitRoot, worktreePath, branchName, baseRef)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func runGitWorktreeAdd(gitRoot, worktreePath, branchName, baseRef string) error {
|
|
cmd := exec.Command("git", "-C", gitRoot, "worktree", "add", "-b", branchName, worktreePath, baseRef)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("git worktree add: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// removeGitWorktree removes a worktree and its branch. Best-effort: logs errors.
|
|
func removeGitWorktree(gitRoot, worktreePath, branchName string, logger *slog.Logger) {
|
|
// Remove the worktree.
|
|
cmd := exec.Command("git", "-C", gitRoot, "worktree", "remove", "--force", worktreePath)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
logger.Warn("execenv: git worktree remove failed", "output", strings.TrimSpace(string(out)), "error", err)
|
|
}
|
|
|
|
// Delete the branch (best-effort).
|
|
if branchName != "" {
|
|
cmd = exec.Command("git", "-C", gitRoot, "branch", "-D", branchName)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
logger.Warn("execenv: git branch delete failed", "branch", branchName, "output", strings.TrimSpace(string(out)), "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// excludeFromGit adds a pattern to the worktree's .git/info/exclude file.
|
|
func excludeFromGit(worktreePath, pattern string) error {
|
|
// Resolve the actual git dir for this worktree.
|
|
cmd := exec.Command("git", "-C", worktreePath, "rev-parse", "--git-dir")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("resolve git dir: %w", err)
|
|
}
|
|
|
|
gitDir := strings.TrimSpace(string(out))
|
|
if !filepath.IsAbs(gitDir) {
|
|
gitDir = filepath.Join(worktreePath, gitDir)
|
|
}
|
|
|
|
excludePath := filepath.Join(gitDir, "info", "exclude")
|
|
|
|
// Ensure the info directory exists.
|
|
if err := os.MkdirAll(filepath.Dir(excludePath), 0o755); err != nil {
|
|
return fmt.Errorf("create info dir: %w", err)
|
|
}
|
|
|
|
// Check if pattern is already present.
|
|
existing, _ := os.ReadFile(excludePath)
|
|
if strings.Contains(string(existing), pattern) {
|
|
return nil
|
|
}
|
|
|
|
f, err := os.OpenFile(excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("open exclude file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if _, err := fmt.Fprintf(f, "\n%s\n", pattern); err != nil {
|
|
return fmt.Errorf("write exclude pattern: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// shortID returns the first 8 characters of a UUID string (dashes stripped).
|
|
func shortID(uuid string) string {
|
|
s := strings.ReplaceAll(uuid, "-", "")
|
|
if len(s) > 8 {
|
|
return s[:8]
|
|
}
|
|
return s
|
|
}
|
|
|
|
var nonAlphanumeric = regexp.MustCompile(`[^a-z0-9]+`)
|
|
|
|
// sanitizeName produces a git-branch-safe name from a human-readable string.
|
|
func sanitizeName(name string) string {
|
|
s := strings.ToLower(strings.TrimSpace(name))
|
|
s = nonAlphanumeric.ReplaceAllString(s, "-")
|
|
s = strings.Trim(s, "-")
|
|
if len(s) > 30 {
|
|
s = s[:30]
|
|
s = strings.TrimRight(s, "-")
|
|
}
|
|
if s == "" {
|
|
s = "agent"
|
|
}
|
|
return s
|
|
}
|