mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +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