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:
Pete Davison
2023-04-06 12:07:57 +01:00
committed by GitHub
parent 719f30219b
commit f22389a824
25 changed files with 678 additions and 242 deletions

View File

@@ -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),
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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})
}
}

View File

@@ -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{}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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.