mirror of
https://github.com/go-task/task.git
synced 2026-07-02 17:08:45 +00:00
feat: implement task sorting with --sort flag (#1105)
* refactor: move deepcopy into its own package * feat: add generic orderedmap implementation * refactor: implement tasks with orderedmap * feat: implement sort flag for all task outputs * refactor: implement vars with orderedmap * chore: docs * fix: linting issues * fix: non deterministic behavior in tests
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Cmd is a task command
|
||||
@@ -27,12 +29,12 @@ func (c *Cmd) DeepCopy() *Cmd {
|
||||
Cmd: c.Cmd,
|
||||
Silent: c.Silent,
|
||||
Task: c.Task,
|
||||
Set: deepCopySlice(c.Set),
|
||||
Shopt: deepCopySlice(c.Shopt),
|
||||
Set: deepcopy.Slice(c.Set),
|
||||
Shopt: deepcopy.Slice(c.Shopt),
|
||||
Vars: c.Vars.DeepCopy(),
|
||||
IgnoreError: c.IgnoreError,
|
||||
Defer: c.Defer,
|
||||
Platforms: deepCopySlice(c.Platforms),
|
||||
Platforms: deepcopy.Slice(c.Platforms),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package taskfile
|
||||
|
||||
type DeepCopier[T any] interface {
|
||||
DeepCopy() T
|
||||
}
|
||||
|
||||
func deepCopySlice[T any](orig []T) []T {
|
||||
if orig == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]T, len(orig))
|
||||
for i, v := range orig {
|
||||
if copyable, ok := any(v).(DeepCopier[T]); ok {
|
||||
c[i] = copyable.DeepCopy()
|
||||
} else {
|
||||
c[i] = v
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func deepCopyMap[K comparable, V any](orig map[K]V) map[K]V {
|
||||
if orig == nil {
|
||||
return nil
|
||||
}
|
||||
c := make(map[K]V, len(orig))
|
||||
for k, v := range orig {
|
||||
if copyable, ok := any(v).(DeepCopier[V]); ok {
|
||||
c[k] = copyable.DeepCopy()
|
||||
} else {
|
||||
c[k] = v
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -30,10 +30,7 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
|
||||
t1.Vars.Merge(t2.Vars)
|
||||
t1.Env.Merge(t2.Env)
|
||||
|
||||
if t1.Tasks == nil {
|
||||
t1.Tasks = make(Tasks)
|
||||
}
|
||||
for k, v := range t2.Tasks {
|
||||
return t2.Tasks.Range(func(k string, v *Task) error {
|
||||
// We do a deep copy of the task struct here to ensure that no data can
|
||||
// be changed elsewhere once the taskfile is merged.
|
||||
task := v.DeepCopy()
|
||||
@@ -67,10 +64,10 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
|
||||
// Add the task to the merged taskfile
|
||||
taskNameWithNamespace := taskNameWithNamespace(k, namespaces...)
|
||||
task.Task = taskNameWithNamespace
|
||||
t1.Tasks[taskNameWithNamespace] = task
|
||||
}
|
||||
t1.Tasks.Set(taskNameWithNamespace, task)
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func taskNameWithNamespace(taskName string, namespaces ...string) string {
|
||||
|
||||
@@ -41,7 +41,7 @@ func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.V
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range envs {
|
||||
if _, ok := env.Mapping[key]; !ok {
|
||||
if ok := env.Exists(key); !ok {
|
||||
env.Set(key, taskfile.Var{Static: value})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,18 +128,22 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range includedTaskfile.Vars.Mapping {
|
||||
// nolint: errcheck
|
||||
includedTaskfile.Vars.Range(func(k string, v taskfile.Var) error {
|
||||
o := v
|
||||
o.Dir = dir
|
||||
includedTaskfile.Vars.Mapping[k] = o
|
||||
}
|
||||
for k, v := range includedTaskfile.Env.Mapping {
|
||||
includedTaskfile.Vars.Set(k, o)
|
||||
return nil
|
||||
})
|
||||
// nolint: errcheck
|
||||
includedTaskfile.Env.Range(func(k string, v taskfile.Var) error {
|
||||
o := v
|
||||
o.Dir = dir
|
||||
includedTaskfile.Env.Mapping[k] = o
|
||||
}
|
||||
includedTaskfile.Env.Set(k, o)
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, task := range includedTaskfile.Tasks {
|
||||
for _, task := range includedTaskfile.Tasks.Values() {
|
||||
task.Dir = filepathext.SmartJoin(dir, task.Dir)
|
||||
task.IncludeVars = includedTask.Vars
|
||||
task.IncludedTaskfileVars = includedTaskfile.Vars
|
||||
@@ -151,10 +155,12 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
|
||||
if includedTaskfile.Tasks.Get("default") != nil && t.Tasks.Get(namespace) == nil {
|
||||
defaultTaskName := fmt.Sprintf("%s:default", namespace)
|
||||
t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, namespace)
|
||||
t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, includedTask.Aliases...)
|
||||
task := t.Tasks.Get(defaultTaskName)
|
||||
task.Aliases = append(task.Aliases, namespace)
|
||||
task.Aliases = append(task.Aliases, includedTask.Aliases...)
|
||||
t.Tasks.Set(defaultTaskName, task)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -179,7 +185,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
// Set the location of the Taskfile
|
||||
t.Location = path
|
||||
|
||||
for _, task := range t.Tasks {
|
||||
for _, task := range t.Tasks.Values() {
|
||||
// If the task is not defined, create a new one
|
||||
if task == nil {
|
||||
task = &taskfile.Task{}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Task represents a task
|
||||
@@ -136,22 +138,22 @@ func (t *Task) DeepCopy() *Task {
|
||||
}
|
||||
c := &Task{
|
||||
Task: t.Task,
|
||||
Cmds: deepCopySlice(t.Cmds),
|
||||
Deps: deepCopySlice(t.Deps),
|
||||
Cmds: deepcopy.Slice(t.Cmds),
|
||||
Deps: deepcopy.Slice(t.Deps),
|
||||
Label: t.Label,
|
||||
Desc: t.Desc,
|
||||
Summary: t.Summary,
|
||||
Aliases: deepCopySlice(t.Aliases),
|
||||
Sources: deepCopySlice(t.Sources),
|
||||
Generates: deepCopySlice(t.Generates),
|
||||
Status: deepCopySlice(t.Status),
|
||||
Preconditions: deepCopySlice(t.Preconditions),
|
||||
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: deepCopySlice(t.Set),
|
||||
Shopt: deepCopySlice(t.Shopt),
|
||||
Set: deepcopy.Slice(t.Set),
|
||||
Shopt: deepcopy.Slice(t.Shopt),
|
||||
Vars: t.Vars.DeepCopy(),
|
||||
Env: t.Env.DeepCopy(),
|
||||
Dotenv: deepCopySlice(t.Dotenv),
|
||||
Dotenv: deepcopy.Slice(t.Dotenv),
|
||||
Silent: t.Silent,
|
||||
Interactive: t.Interactive,
|
||||
Internal: t.Internal,
|
||||
@@ -162,7 +164,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
IncludeVars: t.IncludeVars.DeepCopy(),
|
||||
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
|
||||
IncludedTaskfile: t.IncludedTaskfile.DeepCopy(),
|
||||
Platforms: deepCopySlice(t.Platforms),
|
||||
Platforms: deepcopy.Slice(t.Platforms),
|
||||
Location: t.Location.DeepCopy(),
|
||||
}
|
||||
return c
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/orderedmap"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
@@ -36,13 +37,17 @@ vars:
|
||||
{
|
||||
yamlTaskCall,
|
||||
&taskfile.Cmd{},
|
||||
&taskfile.Cmd{Task: "another-task", Vars: &taskfile.Vars{
|
||||
Keys: []string{"PARAM1", "PARAM2"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "VALUE1"},
|
||||
"PARAM2": {Static: "VALUE2"},
|
||||
&taskfile.Cmd{
|
||||
Task: "another-task", Vars: &taskfile.Vars{
|
||||
OrderedMap: orderedmap.FromMapWithOrder(
|
||||
map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "VALUE1"},
|
||||
"PARAM2": {Static: "VALUE2"},
|
||||
},
|
||||
[]string{"PARAM1", "PARAM2"},
|
||||
),
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
yamlDeferredCmd,
|
||||
@@ -52,12 +57,17 @@ vars:
|
||||
{
|
||||
yamlDeferredCall,
|
||||
&taskfile.Cmd{},
|
||||
&taskfile.Cmd{Task: "some_task", Vars: &taskfile.Vars{
|
||||
Keys: []string{"PARAM1"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "var"},
|
||||
&taskfile.Cmd{
|
||||
Task: "some_task", Vars: &taskfile.Vars{
|
||||
OrderedMap: orderedmap.FromMapWithOrder(
|
||||
map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "var"},
|
||||
},
|
||||
[]string{"PARAM1"},
|
||||
),
|
||||
},
|
||||
}, Defer: true},
|
||||
Defer: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
yamlDep,
|
||||
@@ -67,13 +77,17 @@ vars:
|
||||
{
|
||||
yamlTaskCall,
|
||||
&taskfile.Dep{},
|
||||
&taskfile.Dep{Task: "another-task", Vars: &taskfile.Vars{
|
||||
Keys: []string{"PARAM1", "PARAM2"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "VALUE1"},
|
||||
"PARAM2": {Static: "VALUE2"},
|
||||
&taskfile.Dep{
|
||||
Task: "another-task", Vars: &taskfile.Vars{
|
||||
OrderedMap: orderedmap.FromMapWithOrder(
|
||||
map[string]taskfile.Var{
|
||||
"PARAM1": {Static: "VALUE1"},
|
||||
"PARAM2": {Static: "VALUE2"},
|
||||
},
|
||||
[]string{"PARAM1", "PARAM2"},
|
||||
),
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -4,40 +4,49 @@ import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/orderedmap"
|
||||
)
|
||||
|
||||
// Tasks represents a group of tasks
|
||||
type Tasks map[string]*Task
|
||||
type Tasks struct {
|
||||
orderedmap.OrderedMap[string, *Task]
|
||||
}
|
||||
|
||||
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
tasks := map[string]*Task{}
|
||||
if err := node.Decode(tasks); err != nil {
|
||||
tasks := orderedmap.New[string, *Task]()
|
||||
if err := node.Decode(&tasks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name := range tasks {
|
||||
// nolint: errcheck
|
||||
tasks.Range(func(name string, task *Task) error {
|
||||
// Set the task's name
|
||||
if tasks[name] == nil {
|
||||
tasks[name] = &Task{
|
||||
if task == nil {
|
||||
task = &Task{
|
||||
Task: name,
|
||||
}
|
||||
}
|
||||
tasks[name].Task = name
|
||||
task.Task = name
|
||||
|
||||
// Set the task's location
|
||||
for _, keys := range node.Content {
|
||||
if keys.Value == name {
|
||||
tasks[name].Location = &Location{
|
||||
task.Location = &Location{
|
||||
Line: keys.Line,
|
||||
Column: keys.Column,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.Set(name, task)
|
||||
return nil
|
||||
})
|
||||
|
||||
*t = Tasks(tasks)
|
||||
*t = Tasks{
|
||||
OrderedMap: tasks,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
103
taskfile/var.go
103
taskfile/var.go
@@ -3,80 +3,14 @@ package taskfile
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/orderedmap"
|
||||
)
|
||||
|
||||
// Vars is a string[string] variables map.
|
||||
type Vars struct {
|
||||
Keys []string
|
||||
Mapping map[string]Var
|
||||
}
|
||||
|
||||
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
// NOTE(@andreynering): on this style of custom unmarshalling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
var v Var
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
vs.Set(keyNode.Value, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Vars and copies
|
||||
// data by value from the source struct.
|
||||
func (vs *Vars) DeepCopy() *Vars {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
return &Vars{
|
||||
Keys: deepCopySlice(vs.Keys),
|
||||
Mapping: deepCopyMap(vs.Mapping),
|
||||
}
|
||||
}
|
||||
|
||||
// Merge merges the given Vars into the caller one
|
||||
func (vs *Vars) Merge(other *Vars) {
|
||||
_ = other.Range(func(key string, value Var) error {
|
||||
vs.Set(key, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Set sets a value to a given key
|
||||
func (vs *Vars) Set(key string, value Var) {
|
||||
if vs.Mapping == nil {
|
||||
vs.Mapping = make(map[string]Var, 1)
|
||||
}
|
||||
if !slices.Contains(vs.Keys, key) {
|
||||
vs.Keys = append(vs.Keys, key)
|
||||
}
|
||||
vs.Mapping[key] = value
|
||||
}
|
||||
|
||||
// Range allows you to loop into the vars in its right order
|
||||
func (vs *Vars) Range(yield func(key string, value Var) error) error {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range vs.Keys {
|
||||
if err := yield(k, vs.Mapping[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
orderedmap.OrderedMap[string, Var]
|
||||
}
|
||||
|
||||
// ToCacheMap converts Vars to a map containing only the static
|
||||
@@ -100,12 +34,39 @@ func (vs *Vars) ToCacheMap() (m map[string]any) {
|
||||
return
|
||||
}
|
||||
|
||||
// Len returns the size of the map
|
||||
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Range(f func(k string, v Var) error) error {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
return vs.OrderedMap.Range(f)
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Merge(other *Vars) {
|
||||
if vs == nil || other == nil {
|
||||
return
|
||||
}
|
||||
vs.OrderedMap.Merge(other.OrderedMap)
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Len() int {
|
||||
if vs == nil {
|
||||
return 0
|
||||
}
|
||||
return len(vs.Keys)
|
||||
return vs.OrderedMap.Len()
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Vars and copies
|
||||
// data by value from the source struct.
|
||||
func (vs *Vars) DeepCopy() *Vars {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
return &Vars{
|
||||
OrderedMap: vs.OrderedMap.DeepCopy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Var represents either a static or dynamic variable.
|
||||
|
||||
Reference in New Issue
Block a user