Files
go-task/taskfile/ast/task.go
Valentin Maerten 7cea8e3364 refactor: remove rootDir param, auto-detect .git as boundary
Walk up from task dir to find .git instead of threading rootDir through
Globs, checkers, and itemsFromFor. Gitignore rules are discarded if no
.git is found, matching ripgrep's require_git behavior. This keeps the
Globs signature clean and makes the future .taskignore integration
straightforward (loaded at setup like .taskrc, separate boundary).
2026-06-07 15:02:55 +02:00

260 lines
6.8 KiB
Go

package ast
import (
"fmt"
"regexp"
"strings"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Task represents a task
type Task struct {
Task string `hash:"ignore"`
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Requires *Requires
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool
Interactive bool
Internal bool
Method string
Prefix string `hash:"ignore"`
IgnoreError bool
Gitignore *bool
Run string
Platforms []*Platform
If string
Watch bool
Location *Location
Failfast bool
// Populated during merging
Namespace string `hash:"ignore"`
IncludeVars *Vars
IncludedTaskfileVars *Vars
FullName string `hash:"ignore"`
}
func (t *Task) Name() string {
if t.Label != "" {
return t.Label
}
if t.FullName != "" {
return t.FullName
}
return t.Task
}
func (t *Task) LocalName() string {
name := t.FullName
name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":")
return name
}
// IsSilent returns true if the task has silent mode explicitly enabled.
// Returns false if Silent is nil (not set) or explicitly set to false.
func (t *Task) IsSilent() bool {
return t.Silent != nil && *t.Silent
}
// IsGitignore returns true if the task has gitignore filtering explicitly enabled.
// Returns false if Gitignore is nil (not set) or explicitly set to false.
func (t *Task) IsGitignore() bool {
return t.Gitignore != nil && *t.Gitignore
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
func (t *Task) WildcardMatch(name string) (bool, []string) {
names := append([]string{t.Task}, t.Aliases...)
for _, taskName := range names {
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)"))
regex := regexp.MustCompile(regexStr)
wildcards := regex.FindStringSubmatch(name)
if len(wildcards) == 0 {
continue
}
// Remove the first match, which is the full string
wildcards = wildcards[1:]
wildcardCount := strings.Count(taskName, "*")
if len(wildcards) != wildcardCount {
continue
}
return true, wildcards
}
return false, nil
}
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
// Shortcut syntax for a task with a single command
case yaml.ScalarNode:
var cmd Cmd
if err := node.Decode(&cmd); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
t.Cmds = append(t.Cmds, &cmd)
return nil
// Shortcut syntax for a simple task with a list of commands
case yaml.SequenceNode:
var cmds []*Cmd
if err := node.Decode(&cmds); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
t.Cmds = cmds
return nil
// Full task object
case yaml.MappingNode:
var task struct {
Cmds []*Cmd
Cmd *Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool `yaml:"silent,omitempty"`
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Gitignore *bool `yaml:"gitignore,omitempty"`
Run string
Platforms []*Platform
If string
Requires *Requires
Watch bool
Failfast bool
}
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if task.Cmd != nil {
if task.Cmds != nil {
return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds")
}
t.Cmds = []*Cmd{task.Cmd}
} else {
t.Cmds = task.Cmds
}
t.Deps = task.Deps
t.Label = task.Label
t.Desc = task.Desc
t.Prompt = task.Prompt
t.Summary = task.Summary
t.Aliases = task.Aliases
t.Sources = task.Sources
t.Generates = task.Generates
t.Status = task.Status
t.Preconditions = task.Preconditions
t.Dir = task.Dir
t.Set = task.Set
t.Shopt = task.Shopt
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = deepcopy.Scalar(task.Silent)
t.Interactive = task.Interactive
t.Internal = task.Internal
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Gitignore = deepcopy.Scalar(task.Gitignore)
t.Run = task.Run
t.Platforms = task.Platforms
t.If = task.If
t.Requires = task.Requires
t.Watch = task.Watch
t.Failfast = task.Failfast
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("task")
}
// DeepCopy creates a new instance of Task and copies
// data by value from the source struct.
func (t *Task) DeepCopy() *Task {
if t == nil {
return nil
}
c := &Task{
Task: t.Task,
Cmds: deepcopy.Slice(t.Cmds),
Deps: deepcopy.Slice(t.Deps),
Label: t.Label,
Desc: t.Desc,
Prompt: t.Prompt,
Summary: t.Summary,
Aliases: deepcopy.Slice(t.Aliases),
Sources: deepcopy.Slice(t.Sources),
Generates: deepcopy.Slice(t.Generates),
Status: deepcopy.Slice(t.Status),
Preconditions: deepcopy.Slice(t.Preconditions),
Dir: t.Dir,
Set: deepcopy.Slice(t.Set),
Shopt: deepcopy.Slice(t.Shopt),
Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(),
Dotenv: deepcopy.Slice(t.Dotenv),
Silent: deepcopy.Scalar(t.Silent),
Interactive: t.Interactive,
Internal: t.Internal,
Method: t.Method,
Prefix: t.Prefix,
IgnoreError: t.IgnoreError,
Gitignore: deepcopy.Scalar(t.Gitignore),
Run: t.Run,
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
Platforms: deepcopy.Slice(t.Platforms),
If: t.If,
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FullName: t.FullName,
Watch: t.Watch,
Failfast: t.Failfast,
}
return c
}