1
0
mirror of https://github.com/go-task/task.git synced 2025-08-08 22:36:57 +02: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:
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,10 +4,12 @@
- Fix bug where "up-to-date" logs were not being omitted for silent tasks
(#546, #1107 by @danquah).
- Add `.hg` (Mercurial) to the list of ignored directories when using
`--watch` (#1098 by @misery).
- Add `.hg` (Mercurial) to the list of ignored directories when using `--watch`
(#1098 by @misery).
- More improvements to the release tool (#1096 by @pd93)
- Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93)
- Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by
@pd93)
## v3.23.0 - 2023-03-26

View File

@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/internal/orderedmap"
"github.com/go-task/task/v3/taskfile"
)
@@ -32,12 +33,14 @@ func TestArgsV3(t *testing.T) {
{Task: "task-c"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"FOO", "BAR", "BAZ"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
},
[]string{"FOO", "BAR", "BAZ"},
),
},
},
{
@@ -46,10 +49,12 @@ func TestArgsV3(t *testing.T) {
{Task: "task-a"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"CONTENT"},
Mapping: map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
},
[]string{"CONTENT"},
),
},
},
{
@@ -59,10 +64,12 @@ func TestArgsV3(t *testing.T) {
{Task: "task-b"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"FOO"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
[]string{"FOO"},
),
},
},
{
@@ -83,11 +90,13 @@ func TestArgsV3(t *testing.T) {
{Task: "default"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"FOO", "BAR"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
},
[]string{"FOO", "BAR"},
),
},
},
}
@@ -97,7 +106,8 @@ func TestArgsV3(t *testing.T) {
calls, globals := args.ParseV3(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls)
if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {
assert.Equal(t, test.ExpectedGlobals, globals)
assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys())
assert.Equal(t, test.ExpectedGlobals.Values(), globals.Values())
}
})
}
@@ -123,21 +133,25 @@ func TestArgsV2(t *testing.T) {
{
Task: "task-a",
Vars: &taskfile.Vars{
Keys: []string{"FOO"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
[]string{"FOO"},
),
},
},
{Task: "task-b"},
{
Task: "task-c",
Vars: &taskfile.Vars{
Keys: []string{"BAR", "BAZ"},
Mapping: map[string]taskfile.Var{
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"BAR": {Static: "baz"},
"BAZ": {Static: "foo"},
},
[]string{"BAR", "BAZ"},
),
},
},
},
@@ -148,10 +162,12 @@ func TestArgsV2(t *testing.T) {
{
Task: "task-a",
Vars: &taskfile.Vars{
Keys: []string{"CONTENT"},
Mapping: map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"CONTENT": {Static: "with some spaces"},
},
[]string{"CONTENT"},
),
},
},
},
@@ -163,10 +179,12 @@ func TestArgsV2(t *testing.T) {
{Task: "task-b"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"FOO"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
},
[]string{"FOO"},
),
},
},
{
@@ -187,11 +205,13 @@ func TestArgsV2(t *testing.T) {
{Task: "default"},
},
ExpectedGlobals: &taskfile.Vars{
Keys: []string{"FOO", "BAR"},
Mapping: map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
},
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Static: "bar"},
"BAR": {Static: "baz"},
},
[]string{"FOO", "BAR"},
),
},
},
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
)
@@ -57,6 +58,7 @@ func main() {
list bool
listAll bool
listJson bool
taskSort string
status bool
force bool
watch bool
@@ -81,6 +83,7 @@ func main() {
pflag.BoolVarP(&list, "list", "l", false, "Lists tasks with description of current Taskfile.")
pflag.BoolVarP(&listAll, "list-all", "a", false, "Lists tasks with or without a description.")
pflag.BoolVarP(&listJson, "json", "j", false, "Formats task list as JSON.")
pflag.StringVar(&taskSort, "sort", "", "Changes the order of the tasks when listed.")
pflag.BoolVar(&status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
pflag.BoolVarP(&force, "force", "f", false, "Forces execution even when the task is up-to-date.")
pflag.BoolVarP(&watch, "watch", "w", false, "Enables watch of the given task.")
@@ -160,6 +163,14 @@ func main() {
}
}
var taskSorter sort.TaskSorter
switch taskSort {
case "none":
taskSorter = &sort.Noop{}
case "alphanumeric":
taskSorter = &sort.AlphaNumeric{}
}
e := task.Executor{
Force: force,
Watch: watch,
@@ -179,6 +190,7 @@ func main() {
Stderr: os.Stderr,
OutputStyle: output,
TaskSorter: taskSorter,
}
listOptions := task.NewListOptions(list, listAll, listJson)

View File

@@ -36,6 +36,7 @@ variable
| `-I` | `--interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
| `-l` | `--list` | `bool` | `false` | Lists tasks with description of current Taskfile. |
| `-a` | `--list-all` | `bool` | `false` | Lists tasks with or without a description. |
| | `--sort` | `string` | `default` | Changes the order of the tasks when listed. |
| | `--json` | `bool` | `false` | See [JSON Output](#json-output) |
| `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. |
| | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. |

30
help.go
View File

@@ -7,7 +7,6 @@ import (
"io"
"log"
"os"
"sort"
"strings"
"text/tabwriter"
@@ -16,6 +15,7 @@ import (
"github.com/go-task/task/v3/internal/editors"
"github.com/go-task/task/v3/internal/fingerprint"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile"
)
@@ -129,19 +129,27 @@ func (e *Executor) ListTaskNames(allTasks bool) {
if e.Stdout != nil {
w = e.Stdout
}
// create a string slice from all map values (*taskfile.Task)
s := make([]string, 0, len(e.Taskfile.Tasks))
for _, t := range e.Taskfile.Tasks {
if (allTasks || t.Desc != "") && !t.Internal {
s = append(s, strings.TrimRight(t.Task, ":"))
for _, alias := range t.Aliases {
s = append(s, strings.TrimRight(alias, ":"))
// Get the list of tasks and sort them
tasks := e.Taskfile.Tasks.Values()
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
}
e.TaskSorter.Sort(tasks)
// Create a list of task names
taskNames := make([]string, 0, e.Taskfile.Tasks.Len())
for _, task := range tasks {
if (allTasks || task.Desc != "") && !task.Internal {
taskNames = append(taskNames, strings.TrimRight(task.Task, ":"))
for _, alias := range task.Aliases {
taskNames = append(taskNames, strings.TrimRight(alias, ":"))
}
}
}
// sort and print all task names
sort.Strings(s)
for _, t := range s {
for _, t := range taskNames {
fmt.Fprintln(w, t)
}
}

View File

@@ -1,16 +1,16 @@
package taskfile
package deepcopy
type DeepCopier[T any] interface {
type Copier[T any] interface {
DeepCopy() T
}
func deepCopySlice[T any](orig []T) []T {
func Slice[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 {
if copyable, ok := any(v).(Copier[T]); ok {
c[i] = copyable.DeepCopy()
} else {
c[i] = v
@@ -19,13 +19,13 @@ func deepCopySlice[T any](orig []T) []T {
return c
}
func deepCopyMap[K comparable, V any](orig map[K]V) map[K]V {
func Map[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 {
if copyable, ok := any(v).(Copier[V]); ok {
c[k] = copyable.DeepCopy()
} else {
c[k] = v

View File

@@ -0,0 +1,164 @@
package orderedmap
import (
"fmt"
"golang.org/x/exp/constraints"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/deepcopy"
)
// An OrderedMap is a wrapper around a regular map that maintains an ordered
// list of the map's keys. This allows you to run deterministic and ordered
// operations on the map such as printing/serializing/iterating.
type OrderedMap[K constraints.Ordered, V any] struct {
s []K
m map[K]V
}
// New will create a new OrderedMap of the given type and return it.
func New[K constraints.Ordered, V any]() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: make([]K, 0),
m: make(map[K]V),
}
}
// FromMap will create a new OrderedMap from the given map. Since Golang maps
// are unordered, the order of the created OrderedMap will be random.
func FromMap[K constraints.Ordered, V any](m map[K]V) OrderedMap[K, V] {
om := New[K, V]()
om.m = m
om.s = maps.Keys(m)
return om
}
func FromMapWithOrder[K constraints.Ordered, V any](m map[K]V, order []K) OrderedMap[K, V] {
om := New[K, V]()
if len(m) != len(order) {
panic("length of map and order must be equal")
}
om.m = m
om.s = order
for key := range om.m {
if !slices.Contains(om.s, key) {
panic("order keys must match map keys")
}
}
return om
}
// Len will return the number of items in the map.
func (om *OrderedMap[K, V]) Len() int {
return len(om.s)
}
// Set will set the value for a given key.
func (om *OrderedMap[K, V]) Set(key K, value V) {
if om.m == nil {
om.m = make(map[K]V)
}
if _, ok := om.m[key]; !ok {
om.s = append(om.s, key)
}
om.m[key] = value
}
// Get will return the value for a given key.
// If the key does not exist, it will return the zero value of the value type.
func (om *OrderedMap[K, V]) Get(key K) V {
value, ok := om.m[key]
if !ok {
var zero V
return zero
}
return value
}
// Exists will return whether or not the given key exists.
func (om *OrderedMap[K, V]) Exists(key K) bool {
_, ok := om.m[key]
return ok
}
// Sort will sort the map.
func (om *OrderedMap[K, V]) Sort() {
slices.Sort(om.s)
}
// SortFunc will sort the map using the given function.
func (om *OrderedMap[K, V]) SortFunc(less func(i, j K) bool) {
slices.SortFunc(om.s, less)
}
// Keys will return a slice of the map's keys in order.
func (om *OrderedMap[K, V]) Keys() []K {
return om.s
}
// Values will return a slice of the map's values in order.
func (om *OrderedMap[K, V]) Values() []V {
var values []V
for _, key := range om.s {
values = append(values, om.m[key])
}
return values
}
// Range will iterate over the map and call the given function for each key/value.
func (om *OrderedMap[K, V]) Range(fn func(key K, value V) error) error {
for _, key := range om.s {
if err := fn(key, om.m[key]); err != nil {
return err
}
}
return nil
}
// Merge merges the given Vars into the caller one
func (om *OrderedMap[K, V]) Merge(other OrderedMap[K, V]) {
// nolint: errcheck
other.Range(func(key K, value V) error {
om.Set(key, value)
return nil
})
}
func (om *OrderedMap[K, V]) DeepCopy() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: deepcopy.Slice(om.s),
m: deepcopy.Map(om.m),
}
}
func (om *OrderedMap[K, V]) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
// Even numbers contain the keys
// Odd numbers contain the values
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
// Decode the key
keyNode := node.Content[i]
var k K
if err := keyNode.Decode(&k); err != nil {
return err
}
// Decode the value
valueNode := node.Content[i+1]
var v V
if err := valueNode.Decode(&v); err != nil {
return err
}
// Set the key and value
om.Set(k, v)
}
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
}

View File

@@ -0,0 +1,121 @@
package orderedmap
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestFromMap(t *testing.T) {
m := map[int]string{3: "three", 1: "one", 2: "two"}
om := FromMap(m)
assert.Len(t, om.m, 3)
assert.Len(t, om.s, 3)
assert.ElementsMatch(t, []int{1, 2, 3}, om.s)
for key, value := range m {
assert.Equal(t, om.Get(key), value)
}
}
func TestSetGetExists(t *testing.T) {
om := New[int, string]()
assert.False(t, om.Exists(1))
assert.Equal(t, "", om.Get(1))
om.Set(1, "one")
assert.True(t, om.Exists(1))
assert.Equal(t, "one", om.Get(1))
}
func TestSort(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.Sort()
assert.Equal(t, []int{1, 2, 3}, om.s)
}
func TestSortFunc(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.SortFunc(func(i, j int) bool {
return i > j
})
assert.Equal(t, []int{3, 2, 1}, om.s)
}
func TestKeysValues(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
assert.Equal(t, []int{3, 1, 2}, om.Keys())
assert.Equal(t, []string{"three", "one", "two"}, om.Values())
}
func Range(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
keys := make([]int, 0, len(expectedKeys))
values := make([]string, 0, len(expectedValues))
err := om.Range(func(key int, value string) error {
keys = append(keys, key)
values = append(values, value)
return nil
})
assert.NoError(t, err)
assert.ElementsMatch(t, expectedKeys, keys)
assert.ElementsMatch(t, expectedValues, values)
}
func TestOrderedMapMerge(t *testing.T) {
om1 := New[string, int]()
om1.Set("a", 1)
om1.Set("b", 2)
om2 := New[string, int]()
om2.Set("b", 3)
om2.Set("c", 4)
om1.Merge(om2)
expectedKeys := []string{"a", "b", "c"}
expectedValues := []int{1, 3, 4}
assert.Equal(t, len(expectedKeys), len(om1.s))
assert.Equal(t, len(expectedKeys), len(om1.m))
for i, key := range expectedKeys {
assert.True(t, om1.Exists(key))
assert.Equal(t, expectedValues[i], om1.Get(key))
}
}
func TestUnmarshalYAML(t *testing.T) {
yamlString := `
3: three
1: one
2: two
`
var om OrderedMap[int, string]
err := yaml.Unmarshal([]byte(yamlString), &om)
require.NoError(t, err)
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
assert.Equal(t, expectedKeys, om.Keys())
assert.Equal(t, expectedValues, om.Values())
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/orderedmap"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
@@ -47,10 +48,9 @@ func TestGroup(t *testing.T) {
func TestGroupWithBeginEnd(t *testing.T) {
tmpl := templater.Templater{
Vars: &taskfile.Vars{
Keys: []string{"VAR1"},
Mapping: map[string]taskfile.Var{
OrderedMap: orderedmap.FromMap(map[string]taskfile.Var{
"VAR1": {Static: "example-value"},
},
}),
},
}

44
internal/sort/sorter.go Normal file
View File

@@ -0,0 +1,44 @@
package sort
import (
"sort"
"strings"
"github.com/go-task/task/v3/taskfile"
)
type TaskSorter interface {
Sort([]*taskfile.Task)
}
type Noop struct{}
func (s *Noop) Sort(tasks []*taskfile.Task) {}
type AlphaNumeric struct{}
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
func (s *AlphaNumeric) Sort(tasks []*taskfile.Task) {
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].Task < tasks[j].Task
})
}
type AlphaNumericWithRootTasksFirst struct{}
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
func (s *AlphaNumericWithRootTasksFirst) Sort(tasks []*taskfile.Task) {
sort.Slice(tasks, func(i, j int) bool {
iContainsColon := strings.Contains(tasks[i].Task, ":")
jContainsColon := strings.Contains(tasks[j].Task, ":")
if iContainsColon == jContainsColon {
return tasks[i].Task < tasks[j].Task
}
if !iContainsColon && jContainsColon {
return true
}
return false
})
}

View File

@@ -0,0 +1,77 @@
package sort
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/taskfile"
)
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
task1 := &taskfile.Task{Task: "task1"}
task2 := &taskfile.Task{Task: "task2"}
task3 := &taskfile.Task{Task: "ns1:task3"}
task4 := &taskfile.Task{Task: "ns2:task4"}
task5 := &taskfile.Task{Task: "task5"}
task6 := &taskfile.Task{Task: "ns3:task6"}
tests := []struct {
name string
tasks []*taskfile.Task
want []*taskfile.Task
}{
{
name: "no namespace tasks sorted alphabetically first",
tasks: []*taskfile.Task{task3, task2, task1},
want: []*taskfile.Task{task1, task2, task3},
},
{
name: "namespace tasks sorted alphabetically after non-namespaced tasks",
tasks: []*taskfile.Task{task3, task4, task5},
want: []*taskfile.Task{task5, task3, task4},
},
{
name: "all tasks sorted alphabetically with root tasks first",
tasks: []*taskfile.Task{task6, task5, task4, task3, task2, task1},
want: []*taskfile.Task{task1, task2, task5, task3, task4, task6},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &AlphaNumericWithRootTasksFirst{}
s.Sort(tt.tasks)
assert.Equal(t, tt.want, tt.tasks)
})
}
}
func TestAlphaNumeric_Sort(t *testing.T) {
task1 := &taskfile.Task{Task: "task1"}
task2 := &taskfile.Task{Task: "task2"}
task3 := &taskfile.Task{Task: "ns1:task3"}
task4 := &taskfile.Task{Task: "ns2:task4"}
task5 := &taskfile.Task{Task: "task5"}
task6 := &taskfile.Task{Task: "ns3:task6"}
tests := []struct {
name string
tasks []*taskfile.Task
want []*taskfile.Task
}{
{
name: "all tasks sorted alphabetically",
tasks: []*taskfile.Task{task3, task2, task5, task1, task4, task6},
want: []*taskfile.Task{task3, task4, task6, task1, task2, task5},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &AlphaNumeric{}
s.Sort(tt.tasks)
assert.Equal(t, tt.tasks, tt.want)
})
}
}

View File

@@ -10,7 +10,7 @@ import (
func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) {
for i, call := range c {
PrintSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks[call.Task])
PrintTask(l, t.Tasks.Get(call.Task))
}
}

View File

@@ -156,10 +156,10 @@ func TestPrintAllWithSpaces(t *testing.T) {
t2 := &taskfile.Task{Task: "t2"}
t3 := &taskfile.Task{Task: "t3"}
tasks := make(taskfile.Tasks, 3)
tasks["t1"] = t1
tasks["t2"] = t2
tasks["t3"] = t3
tasks := taskfile.Tasks{}
tasks.Set("t1", t1)
tasks.Set("t2", t2)
tasks.Set("t3", t3)
summary.PrintTasks(&l,
&taskfile.Taskfile{Tasks: tasks},

View File

@@ -94,10 +94,10 @@ func (e *Executor) setupFuzzyModel() {
model.SetThreshold(1) // because we want to build grammar based on every task name
var words []string
for taskName := range e.Taskfile.Tasks {
for _, taskName := range e.Taskfile.Tasks.Keys() {
words = append(words, taskName)
for _, task := range e.Taskfile.Tasks {
for _, task := range e.Taskfile.Tasks.Values() {
words = append(words, task.Aliases...)
}
}
@@ -202,7 +202,7 @@ func (e *Executor) readDotEnvFiles() error {
}
err = env.Range(func(key string, value taskfile.Var) error {
if _, ok := e.Taskfile.Env.Mapping[key]; !ok {
if ok := e.Taskfile.Env.Exists(key); !ok {
e.Taskfile.Env.Set(key, value)
}
return nil
@@ -232,9 +232,9 @@ func (e *Executor) setupDefaults() {
func (e *Executor) setupConcurrencyState() {
e.executionHashes = make(map[string]context.Context)
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
for _, k := range e.Taskfile.Tasks.Keys() {
e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{}
}
@@ -281,7 +281,7 @@ func (e *Executor) doVersionChecks() error {
if v.Compare(semver.MustParse("2.1")) <= 0 {
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks {
for _, task := range e.Taskfile.Tasks.Values() {
if task.IgnoreError {
return err
}
@@ -294,7 +294,7 @@ func (e *Executor) doVersionChecks() error {
}
if v.LessThan(semver.MustParse("2.6")) {
for _, task := range e.Taskfile.Tasks {
for _, task := range e.Taskfile.Tasks.Values() {
if len(task.Preconditions) > 0 {
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
}
@@ -318,7 +318,7 @@ func (e *Executor) doVersionChecks() error {
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
}
for _, task := range e.Taskfile.Tasks {
for _, task := range e.Taskfile.Tasks.Values() {
if task.Run != "" {
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
}

58
task.go
View File

@@ -6,8 +6,6 @@ import (
"io"
"os"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
@@ -19,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
@@ -60,6 +59,7 @@ type Executor struct {
Compiler compiler.Compiler
Output output.Output
OutputStyle taskfile.Output
TaskSorter sort.TaskSorter
taskvars *taskfile.Vars
fuzzyModel *fuzzy.Model
@@ -357,14 +357,14 @@ func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute
// If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
// Search for a matching task
matchingTask, ok := e.Taskfile.Tasks[call.Task]
if ok {
matchingTask := e.Taskfile.Tasks.Get(call.Task)
if matchingTask != nil {
return matchingTask, nil
}
// If didn't find one, search for a task with a matching alias
var aliasedTasks []string
for _, task := range e.Taskfile.Tasks {
for _, task := range e.Taskfile.Tasks.Values() {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task
@@ -395,28 +395,33 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
type FilterFunc func(task *taskfile.Task) bool
func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) {
tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
tasks := make([]*taskfile.Task, 0, e.Taskfile.Tasks.Len())
// Create an error group to wait for each task to be compiled
var g errgroup.Group
// Fetch and compile the list of tasks
for key := range e.Taskfile.Tasks {
task := e.Taskfile.Tasks[key]
g.Go(func() error {
// Check if we should filter the task
for _, filter := range filters {
if filter(task) {
return nil
}
// Filter tasks based on the given filter functions
for _, task := range e.Taskfile.Tasks.Values() {
var shouldFilter bool
for _, filter := range filters {
if filter(task) {
shouldFilter = true
}
}
if !shouldFilter {
tasks = append(tasks, task)
}
}
// Compile the task
// Compile the list of tasks
for i := range tasks {
task := tasks[i]
g.Go(func() error {
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
if err == nil {
task = compiledTask
}
tasks = append(tasks, task)
task = compiledTask
return nil
})
}
@@ -426,20 +431,11 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error)
return nil, err
}
// Sort the tasks.
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
sort.Slice(tasks, func(i, j int) bool {
iContainsColon := strings.Contains(tasks[i].Task, ":")
jContainsColon := strings.Contains(tasks[j].Task, ":")
if iContainsColon == jContainsColon {
return tasks[i].Task < tasks[j].Task
}
if !iContainsColon && jContainsColon {
return true
}
return false
})
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
}
e.TaskSorter.Sort(tasks)
return tasks, nil
}

View File

@@ -790,7 +790,7 @@ func TestTaskVersion(t *testing.T) {
}
require.NoError(t, e.Setup())
assert.Equal(t, test.Version, e.Taskfile.Version)
assert.Equal(t, 2, len(e.Taskfile.Tasks))
assert.Equal(t, 2, e.Taskfile.Tasks.Len())
})
}
}

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

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

View File

@@ -91,7 +91,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
return nil, err
}
for key, value := range envs {
if _, ok := dotenvEnvs.Mapping[key]; !ok {
if ok := dotenvEnvs.Exists(key); !ok {
dotenvEnvs.Set(key, taskfile.Var{Static: value})
}
}