mirror of
https://github.com/go-task/task.git
synced 2025-12-16 11:47:44 +01:00
209 lines
4.9 KiB
Go
209 lines
4.9 KiB
Go
package task
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"github.com/go-task/task/v3/errors"
|
|
"github.com/go-task/task/v3/internal/input"
|
|
"github.com/go-task/task/v3/internal/term"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
func (e *Executor) canPrompt() bool {
|
|
return e.Interactive && (e.AssumeTerm || term.IsTerminal())
|
|
}
|
|
|
|
func (e *Executor) newPrompter() *input.Prompter {
|
|
return &input.Prompter{
|
|
Stdin: e.Stdin,
|
|
Stdout: e.Stdout,
|
|
Stderr: e.Stderr,
|
|
}
|
|
}
|
|
|
|
// promptDepsVars traverses the dependency tree, collects all missing required
|
|
// variables, and prompts for them upfront. This is used for deps which execute
|
|
// in parallel, so all prompts must happen before execution to avoid interleaving.
|
|
// Prompted values are stored in e.promptedVars for injection into task calls.
|
|
func (e *Executor) promptDepsVars(calls []*Call) error {
|
|
if !e.canPrompt() {
|
|
return nil
|
|
}
|
|
|
|
// Collect all missing vars from the dependency tree
|
|
visited := make(map[string]bool)
|
|
varsMap := make(map[string]*ast.VarsWithValidation)
|
|
|
|
var collect func(call *Call) error
|
|
collect = func(call *Call) error {
|
|
compiledTask, err := e.FastCompiledTask(call)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range getMissingRequiredVars(compiledTask) {
|
|
if _, exists := varsMap[v.Name]; !exists {
|
|
varsMap[v.Name] = v
|
|
}
|
|
}
|
|
|
|
// Check visited AFTER collecting vars to handle duplicate task calls with different vars
|
|
if visited[call.Task] {
|
|
return nil
|
|
}
|
|
visited[call.Task] = true
|
|
|
|
for _, dep := range compiledTask.Deps {
|
|
depCall := &Call{
|
|
Task: dep.Task,
|
|
Vars: dep.Vars,
|
|
Silent: dep.Silent,
|
|
}
|
|
if err := collect(depCall); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, call := range calls {
|
|
if err := collect(call); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(varsMap) == 0 {
|
|
return nil
|
|
}
|
|
|
|
prompter := e.newPrompter()
|
|
e.promptedVars = ast.NewVars()
|
|
|
|
for _, v := range varsMap {
|
|
value, err := prompter.Prompt(v.Name, v.Enum)
|
|
if err != nil {
|
|
if errors.Is(err, input.ErrCancelled) {
|
|
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
|
|
}
|
|
return err
|
|
}
|
|
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// promptTaskVars prompts for any missing required vars from a single task.
|
|
// Used for sequential task calls (cmds) where we can prompt just-in-time.
|
|
// Returns true if any vars were prompted (caller should recompile the task).
|
|
func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
|
|
if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// Find missing vars, excluding already prompted ones
|
|
var missing []*ast.VarsWithValidation
|
|
for _, v := range getMissingRequiredVars(t) {
|
|
if e.promptedVars != nil {
|
|
if _, ok := e.promptedVars.Get(v.Name); ok {
|
|
continue
|
|
}
|
|
}
|
|
missing = append(missing, v)
|
|
}
|
|
|
|
if len(missing) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
prompter := e.newPrompter()
|
|
|
|
for _, v := range missing {
|
|
value, err := prompter.Prompt(v.Name, v.Enum)
|
|
if err != nil {
|
|
if errors.Is(err, input.ErrCancelled) {
|
|
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// Add to call.Vars for recompilation
|
|
if call.Vars == nil {
|
|
call.Vars = ast.NewVars()
|
|
}
|
|
call.Vars.Set(v.Name, ast.Var{Value: value})
|
|
|
|
// Cache for reuse by other tasks
|
|
if e.promptedVars == nil {
|
|
e.promptedVars = ast.NewVars()
|
|
}
|
|
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// getMissingRequiredVars returns required vars that are not set in the task's vars.
|
|
func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {
|
|
if t.Requires == nil {
|
|
return nil
|
|
}
|
|
var missing []*ast.VarsWithValidation
|
|
for _, v := range t.Requires.Vars {
|
|
if _, ok := t.Vars.Get(v.Name); !ok {
|
|
missing = append(missing, v)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
|
missing := getMissingRequiredVars(t)
|
|
if len(missing) == 0 {
|
|
return nil
|
|
}
|
|
|
|
missingVars := make([]errors.MissingVar, len(missing))
|
|
for i, v := range missing {
|
|
missingVars[i] = errors.MissingVar{
|
|
Name: v.Name,
|
|
AllowedValues: v.Enum,
|
|
}
|
|
}
|
|
|
|
return &errors.TaskMissingRequiredVarsError{
|
|
TaskName: t.Name(),
|
|
MissingVars: missingVars,
|
|
}
|
|
}
|
|
|
|
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
|
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var notAllowedValuesVars []errors.NotAllowedVar
|
|
for _, requiredVar := range t.Requires.Vars {
|
|
varValue, _ := t.Vars.Get(requiredVar.Name)
|
|
|
|
value, isString := varValue.Value.(string)
|
|
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
|
|
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
|
|
Value: value,
|
|
Enum: requiredVar.Enum,
|
|
Name: requiredVar.Name,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(notAllowedValuesVars) > 0 {
|
|
return &errors.TaskNotAllowedVarsError{
|
|
TaskName: t.Name(),
|
|
NotAllowedVars: notAllowedValuesVars,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|