Files
go-task/taskfile/ast/task.go
Valentin Maerten fe542d5418 feat: add gitignore option to exclude ignored files from sources/generates
When `gitignore: true` is set at the Taskfile or task level, files
matching .gitignore rules are automatically excluded from sources and
generates glob resolution. This prevents rebuilds triggered by changes
to files that are in .gitignore (build artifacts, generated files, etc.).

Uses go-git to load .gitignore patterns including nested .gitignore
files, .git/info/exclude, and global gitignore configuration.
2026-06-07 15:02:39 +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
}