// Package shell contiene operaciones I/O del parallel executor. // Todas las funciones retornan Result[T] y tienen side effects (git, filesystem). package shell import ( "context" "fmt" "os" "strings" "time" "github.com/lucasdataproyects/devfactory/core" dfshell "github.com/lucasdataproyects/devfactory/shell" pcore "github.com/lucasdataproyects/parallel-executor/core" ) // CreateWorktree crea un git worktree para una issue. // Crea branch desde master y configura el directorio de trabajo. func CreateWorktree(spec pcore.WorktreeSpec, repoRoot string) core.Result[string] { // Verificar que no existe ya if dfshell.DirExists(spec.WorkDir) { return core.Ok(spec.WorkDir) } // Crear directorio padre si no existe parentDir := spec.WorkDir[:strings.LastIndex(spec.WorkDir, "/")] mkResult := dfshell.MkdirAll(parentDir) if mkResult.IsErr() { return core.Err[string](mkResult.Error()) } // Actualizar master primero ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() updateResult := dfshell.RunWithContext(ctx, "git", "-C", repoRoot, "fetch", "origin", "master") if updateResult.IsErr() { // No fatal — puede no tener remote } // Crear worktree con nueva branch desde master result := dfshell.Run("git", "-C", repoRoot, "worktree", "add", "-b", spec.BranchName, spec.WorkDir, "master", ) if result.IsErr() { // Branch puede existir — intentar sin -b result = dfshell.Run("git", "-C", repoRoot, "worktree", "add", spec.WorkDir, spec.BranchName, ) if result.IsErr() { return core.Err[string](fmt.Errorf("failed to create worktree for issue #%04d: %w", spec.Issue.Number, result.Error())) } } return core.Ok(spec.WorkDir) } // RemoveWorktree elimina un worktree y su branch. func RemoveWorktree(spec pcore.WorktreeSpec, repoRoot string) core.Result[struct{}] { if !dfshell.DirExists(spec.WorkDir) { return core.Ok(struct{}{}) } // Remover worktree result := dfshell.Run("git", "-C", repoRoot, "worktree", "remove", "--force", spec.WorkDir, ) if result.IsErr() { // Fallback: eliminar directorio manualmente os.RemoveAll(spec.WorkDir) // Prune worktrees huérfanos dfshell.Run("git", "-C", repoRoot, "worktree", "prune") } return core.Ok(struct{}{}) } // CleanupAllWorktrees limpia todos los worktrees del directorio worktrees/. func CleanupAllWorktrees(repoRoot string) core.Result[int] { worktreesDir := repoRoot + "/worktrees" if !dfshell.DirExists(worktreesDir) { return core.Ok(0) } entries := dfshell.ListDir(worktreesDir) if entries.IsErr() { return core.Ok(0) } count := 0 for _, entry := range entries.Unwrap() { fullPath := worktreesDir + "/" + entry dfshell.Run("git", "-C", repoRoot, "worktree", "remove", "--force", fullPath) count++ } // Prune dfshell.Run("git", "-C", repoRoot, "worktree", "prune") // Eliminar directorio vacío os.Remove(worktreesDir) return core.Ok(count) } // ListWorktrees devuelve los worktrees activos. func ListWorktrees(repoRoot string) core.Result[[]string] { result := dfshell.Run("git", "-C", repoRoot, "worktree", "list", "--porcelain") if result.IsErr() { return core.Err[[]string](result.Error()) } output := result.Unwrap().Output() lines := strings.Split(output, "\n") var paths []string for _, line := range lines { if strings.HasPrefix(line, "worktree ") { path := strings.TrimPrefix(line, "worktree ") if path != repoRoot { paths = append(paths, path) } } } return core.Ok(paths) }