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 }