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:
@@ -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
|
||||
|
||||
|
@@ -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"},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
30
help.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
164
internal/orderedmap/orderedmap.go
Normal file
164
internal/orderedmap/orderedmap.go
Normal 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())
|
||||
}
|
121
internal/orderedmap/orderedmap_test.go
Normal file
121
internal/orderedmap/orderedmap_test.go
Normal 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())
|
||||
}
|
@@ -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
44
internal/sort/sorter.go
Normal 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
|
||||
})
|
||||
}
|
77
internal/sort/sorter_test.go
Normal file
77
internal/sort/sorter_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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},
|
||||
|
18
setup.go
18
setup.go
@@ -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
58
task.go
@@ -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
|
||||
}
|
||||
|
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user