Repository: go-task/task
Pull request #1456 (2024-01-04 16:31:25)

refactor: bits from the dag branch
This PR is an assortment of cherry-picked changes from the `dag` branch. None of these are directly related to the dag functionality, but were made because they benefit its design. The idea being that when I (eventually) post the dag PR, the changes will be much easier to understand and only related to the dag functionality itself.

- refactor: `IncludedTaskfiles` -> `Includes`
  - The idea of this is just that it keeps the consistency in naming with the key used in the Taskfile schema. It also means less use of variables with the word "taskfile" in them, which can get confusing because it's not actually a taskfile, but a reference to one.
- feat: store namespace directly in `Include`
  - This has benefits in the dag code since the namespace is required when merging the taskfiles in the graph together and the complete `Include` struct can be easily stored on a graph edge. However, it also helps tidy up the code a bit without the dag code.
- feat: `Includes` should use `OrderedMap`
  - Not really related to dag. Just a nice tidy up. No need for multiple ordered map implementations.
- refactor: `orderedmap` package -> `omap`
  - More go-like package name IMO.
- refactor: split `read.go` into `taskfile.go` and `reader.go`
  - This is very specific to the dag changes and is really only there because it will help with reading the diffs as there is a dedicated `Reader` in the `dag` branch and code was moved around.
- refactor: move prompts into consts
- fix: getSpecialVars



Patch:
diff --git a/internal/orderedmap/orderedmap.go b/internal/omap/orderedmap.go
similarity index 99%
rename from internal/orderedmap/orderedmap.go
rename to internal/omap/orderedmap.go
index 8a497f6b..c57b9023 100644
--- a/internal/orderedmap/orderedmap.go
+++ b/internal/omap/orderedmap.go
@@ -1,4 +1,4 @@
-package orderedmap
+package omap
 
 import (
 	"fmt"
diff --git a/taskfile/ast/included_taskfile.go b/taskfile/ast/include.go
similarity index 50%
rename from taskfile/ast/included_taskfile.go
rename to taskfile/ast/include.go
index aea7d6ef..b55762cc 100644
--- a/taskfile/ast/included_taskfile.go
+++ b/taskfile/ast/include.go
@@ -7,13 +7,14 @@ import (
 
 	"github.com/go-task/task/v3/internal/execext"
 	"github.com/go-task/task/v3/internal/filepathext"
+	omap "github.com/go-task/task/v3/internal/omap"
 
-	"golang.org/x/exp/slices"
 	"gopkg.in/yaml.v3"
 )
 
-// IncludedTaskfile represents information about included taskfiles
-type IncludedTaskfile struct {
+// Include represents information about included taskfiles
+type Include struct {
+	Namespace      string
 	Taskfile       string
 	Dir            string
 	Optional       bool
@@ -24,14 +25,13 @@ type IncludedTaskfile struct {
 	BaseDir        string // The directory from which the including taskfile was loaded; used to resolve relative paths
 }
 
-// IncludedTaskfiles represents information about included tasksfiles
-type IncludedTaskfiles struct {
-	Keys    []string
-	Mapping map[string]IncludedTaskfile
+// Includes represents information about included tasksfiles
+type Includes struct {
+	omap.OrderedMap[string, Include]
 }
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (tfs *IncludedTaskfiles) UnmarshalYAML(node *yaml.Node) error {
+func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
 	switch node.Kind {
 	case yaml.MappingNode:
 		// NOTE(@andreynering): on this style of custom unmarshalling,
@@ -41,11 +41,12 @@ func (tfs *IncludedTaskfiles) UnmarshalYAML(node *yaml.Node) error {
 			keyNode := node.Content[i]
 			valueNode := node.Content[i+1]
 
-			var v IncludedTaskfile
+			var v Include
 			if err := valueNode.Decode(&v); err != nil {
 				return err
 			}
-			tfs.Set(keyNode.Value, v)
+			v.Namespace = keyNode.Value
+			includes.Set(keyNode.Value, v)
 		}
 		return nil
 	}
@@ -54,38 +55,22 @@ func (tfs *IncludedTaskfiles) UnmarshalYAML(node *yaml.Node) error {
 }
 
 // Len returns the length of the map
-func (tfs *IncludedTaskfiles) Len() int {
-	if tfs == nil {
+func (includes *Includes) Len() int {
+	if includes == nil {
 		return 0
 	}
-	return len(tfs.Keys)
+	return includes.OrderedMap.Len()
 }
 
-// Set sets a value to a given key
-func (tfs *IncludedTaskfiles) Set(key string, includedTaskfile IncludedTaskfile) {
-	if tfs.Mapping == nil {
-		tfs.Mapping = make(map[string]IncludedTaskfile, 1)
-	}
-	if !slices.Contains(tfs.Keys, key) {
-		tfs.Keys = append(tfs.Keys, key)
-	}
-	tfs.Mapping[key] = includedTaskfile
-}
-
-// Range allows you to loop into the included taskfiles in its right order
-func (tfs *IncludedTaskfiles) Range(yield func(key string, includedTaskfile IncludedTaskfile) error) error {
-	if tfs == nil {
+// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
+func (includes *Includes) Range(f func(k string, v Include) error) error {
+	if includes == nil {
 		return nil
 	}
-	for _, k := range tfs.Keys {
-		if err := yield(k, tfs.Mapping[k]); err != nil {
-			return err
-		}
-	}
-	return nil
+	return includes.OrderedMap.Range(f)
 }
 
-func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
+func (include *Include) UnmarshalYAML(node *yaml.Node) error {
 	switch node.Kind {
 
 	case yaml.ScalarNode:
@@ -93,7 +78,7 @@ func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
 		if err := node.Decode(&str); err != nil {
 			return err
 		}
-		it.Taskfile = str
+		include.Taskfile = str
 		return nil
 
 	case yaml.MappingNode:
@@ -108,13 +93,13 @@ func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
 		if err := node.Decode(&includedTaskfile); err != nil {
 			return err
 		}
-		it.Taskfile = includedTaskfile.Taskfile
-		it.Dir = includedTaskfile.Dir
-		it.Optional = includedTaskfile.Optional
-		it.Internal = includedTaskfile.Internal
-		it.Aliases = includedTaskfile.Aliases
-		it.AdvancedImport = true
-		it.Vars = includedTaskfile.Vars
+		include.Taskfile = includedTaskfile.Taskfile
+		include.Dir = includedTaskfile.Dir
+		include.Optional = includedTaskfile.Optional
+		include.Internal = includedTaskfile.Internal
+		include.Aliases = includedTaskfile.Aliases
+		include.AdvancedImport = true
+		include.Vars = includedTaskfile.Vars
 		return nil
 	}
 
@@ -123,34 +108,35 @@ func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
 
 // DeepCopy creates a new instance of IncludedTaskfile and copies
 // data by value from the source struct.
-func (it *IncludedTaskfile) DeepCopy() *IncludedTaskfile {
-	if it == nil {
+func (include *Include) DeepCopy() *Include {
+	if include == nil {
 		return nil
 	}
-	return &IncludedTaskfile{
-		Taskfile:       it.Taskfile,
-		Dir:            it.Dir,
-		Optional:       it.Optional,
-		Internal:       it.Internal,
-		AdvancedImport: it.AdvancedImport,
-		Vars:           it.Vars.DeepCopy(),
-		BaseDir:        it.BaseDir,
+	return &Include{
+		Namespace:      include.Namespace,
+		Taskfile:       include.Taskfile,
+		Dir:            include.Dir,
+		Optional:       include.Optional,
+		Internal:       include.Internal,
+		AdvancedImport: include.AdvancedImport,
+		Vars:           include.Vars.DeepCopy(),
+		BaseDir:        include.BaseDir,
 	}
 }
 
 // FullTaskfilePath returns the fully qualified path to the included taskfile
-func (it *IncludedTaskfile) FullTaskfilePath() (string, error) {
-	return it.resolvePath(it.Taskfile)
+func (include *Include) FullTaskfilePath() (string, error) {
+	return include.resolvePath(include.Taskfile)
 }
 
 // FullDirPath returns the fully qualified path to the included taskfile's working directory
-func (it *IncludedTaskfile) FullDirPath() (string, error) {
-	return it.resolvePath(it.Dir)
+func (include *Include) FullDirPath() (string, error) {
+	return include.resolvePath(include.Dir)
 }
 
-func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
+func (include *Include) resolvePath(path string) (string, error) {
 	// If the file is remote, we don't need to resolve the path
-	if strings.Contains(it.Taskfile, "://") {
+	if strings.Contains(include.Taskfile, "://") {
 		return path, nil
 	}
 
@@ -163,9 +149,9 @@ func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
 		return path, nil
 	}
 
-	result, err := filepath.Abs(filepathext.SmartJoin(it.BaseDir, path))
+	result, err := filepath.Abs(filepathext.SmartJoin(include.BaseDir, path))
 	if err != nil {
-		return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, it.BaseDir, err)
+		return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, include.BaseDir, err)
 	}
 
 	return result, nil
diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go
index 1a289337..4e28625f 100644
--- a/taskfile/ast/task.go
+++ b/taskfile/ast/task.go
@@ -38,7 +38,7 @@ type Task struct {
 	Run                  string
 	IncludeVars          *Vars
 	IncludedTaskfileVars *Vars
-	IncludedTaskfile     *IncludedTaskfile
+	IncludedTaskfile     *Include
 	Platforms            []*Platform
 	Location             *Location
 	Watch                bool
diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go
index 7595b2c8..1e62c7ad 100644
--- a/taskfile/ast/taskfile.go
+++ b/taskfile/ast/taskfile.go
@@ -18,7 +18,7 @@ type Taskfile struct {
 	Version  *semver.Version
 	Output   Output
 	Method   string
-	Includes *IncludedTaskfiles
+	Includes *Includes
 	Set      []string
 	Shopt    []string
 	Vars     *Vars
@@ -37,7 +37,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
 			Version  *semver.Version
 			Output   Output
 			Method   string
-			Includes *IncludedTaskfiles
+			Includes *Includes
 			Set      []string
 			Shopt    []string
 			Vars     *Vars
diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go
index 2f5906e7..4f632428 100644
--- a/taskfile/ast/tasks.go
+++ b/taskfile/ast/tasks.go
@@ -5,18 +5,18 @@ import (
 
 	"gopkg.in/yaml.v3"
 
-	"github.com/go-task/task/v3/internal/orderedmap"
+	"github.com/go-task/task/v3/internal/omap"
 )
 
 // Tasks represents a group of tasks
 type Tasks struct {
-	orderedmap.OrderedMap[string, *Task]
+	omap.OrderedMap[string, *Task]
 }
 
 func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
 	switch node.Kind {
 	case yaml.MappingNode:
-		tasks := orderedmap.New[string, *Task]()
+		tasks := omap.New[string, *Task]()
 		if err := node.Decode(&tasks); err != nil {
 			return err
 		}
diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go
index 6b827af5..93f883da 100644
--- a/taskfile/ast/var.go
+++ b/taskfile/ast/var.go
@@ -7,12 +7,12 @@ import (
 	"gopkg.in/yaml.v3"
 
 	"github.com/go-task/task/v3/internal/experiments"
-	"github.com/go-task/task/v3/internal/orderedmap"
+	"github.com/go-task/task/v3/internal/omap"
 )
 
 // Vars is a string[string] variables map.
 type Vars struct {
-	orderedmap.OrderedMap[string, Var]
+	omap.OrderedMap[string, Var]
 }
 
 // ToCacheMap converts Vars to a map containing only the static
diff --git a/taskfile/merge.go b/taskfile/merge.go
index 3836fc22..1f877157 100644
--- a/taskfile/merge.go
+++ b/taskfile/merge.go
@@ -11,7 +11,7 @@ import (
 const NamespaceSeparator = ":"
 
 // Merge merges the second Taskfile into the first
-func Merge(t1, t2 *ast.Taskfile, includedTaskfile *ast.IncludedTaskfile, namespaces ...string) error {
+func Merge(t1, t2 *ast.Taskfile, include *ast.Include) error {
 	if !t1.Version.Equal(t2.Version) {
 		return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
 	}
@@ -28,32 +28,32 @@ func Merge(t1, t2 *ast.Taskfile, includedTaskfile *ast.IncludedTaskfile, namespa
 	t1.Vars.Merge(t2.Vars)
 	t1.Env.Merge(t2.Env)
 
-	return t2.Tasks.Range(func(k string, v *ast.Task) error {
+	if err := t2.Tasks.Range(func(k string, v *ast.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()
 
 		// Set the task to internal if EITHER the included task or the included
 		// taskfile are marked as internal
-		task.Internal = task.Internal || (includedTaskfile != nil && includedTaskfile.Internal)
+		task.Internal = task.Internal || (include != nil && include.Internal)
 
 		// Add namespaces to dependencies, commands and aliases
 		for _, dep := range task.Deps {
 			if dep != nil && dep.Task != "" {
-				dep.Task = taskNameWithNamespace(dep.Task, namespaces...)
+				dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
 			}
 		}
 		for _, cmd := range task.Cmds {
 			if cmd != nil && cmd.Task != "" {
-				cmd.Task = taskNameWithNamespace(cmd.Task, namespaces...)
+				cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
 			}
 		}
 		for i, alias := range task.Aliases {
-			task.Aliases[i] = taskNameWithNamespace(alias, namespaces...)
+			task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
 		}
 		// Add namespace aliases
-		if includedTaskfile != nil {
-			for _, namespaceAlias := range includedTaskfile.Aliases {
+		if include != nil {
+			for _, namespaceAlias := range include.Aliases {
 				task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
 				for _, alias := range v.Aliases {
 					task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
@@ -62,17 +62,32 @@ func Merge(t1, t2 *ast.Taskfile, includedTaskfile *ast.IncludedTaskfile, namespa
 		}
 
 		// Add the task to the merged taskfile
-		taskNameWithNamespace := taskNameWithNamespace(k, namespaces...)
+		taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace)
 		task.Task = taskNameWithNamespace
 		t1.Tasks.Set(taskNameWithNamespace, task)
 
 		return nil
-	})
+	}); err != nil {
+		return err
+	}
+
+	// If the included Taskfile has a default task and the parent namespace has
+	// no task with a matching name, we can add an alias so that the user can
+	// run the included Taskfile's default task without specifying its full
+	// name. If the parent namespace has aliases, we add another alias for each
+	// of them.
+	if t2.Tasks.Get("default") != nil && t1.Tasks.Get(include.Namespace) == nil {
+		defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
+		t1.Tasks.Get(defaultTaskName).Aliases = append(t1.Tasks.Get(defaultTaskName).Aliases, include.Namespace)
+		t1.Tasks.Get(defaultTaskName).Aliases = append(t1.Tasks.Get(defaultTaskName).Aliases, include.Aliases...)
+	}
+
+	return nil
 }
 
-func taskNameWithNamespace(taskName string, namespaces ...string) string {
+func taskNameWithNamespace(taskName string, namespace string) string {
 	if strings.HasPrefix(taskName, NamespaceSeparator) {
 		return strings.TrimPrefix(taskName, NamespaceSeparator)
 	}
-	return strings.Join(append(namespaces, taskName), NamespaceSeparator)
+	return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName)
 }
diff --git a/taskfile/read.go b/taskfile/reader.go
similarity index 59%
rename from taskfile/read.go
rename to taskfile/reader.go
index 44260c95..13d83256 100644
--- a/taskfile/read.go
+++ b/taskfile/reader.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"path/filepath"
 	"time"
 
 	"gopkg.in/yaml.v3"
@@ -12,130 +11,19 @@ import (
 	"github.com/go-task/task/v3/errors"
 	"github.com/go-task/task/v3/internal/filepathext"
 	"github.com/go-task/task/v3/internal/logger"
-	"github.com/go-task/task/v3/internal/sysinfo"
 	"github.com/go-task/task/v3/internal/templater"
 	"github.com/go-task/task/v3/taskfile/ast"
 )
 
-var (
-	// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
-	ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
-
-	defaultTaskfiles = []string{
-		"Taskfile.yml",
-		"taskfile.yml",
-		"Taskfile.yaml",
-		"taskfile.yaml",
-		"Taskfile.dist.yml",
-		"taskfile.dist.yml",
-		"Taskfile.dist.yaml",
-		"taskfile.dist.yaml",
-	}
+const (
+	taskfileUntrustedPrompt = `The task you are attempting to run depends on the remote Taskfile at %q.
+	--- Make sure you trust the source of this Taskfile before continuing ---
+	Continue?`
+	taskfileChangedPrompt = `The Taskfile at %q has changed since you last used it!
+	--- Make sure you trust the source of this Taskfile before continuing ---
+	Continue?`
 )
 
-func readTaskfile(
-	node Node,
-	download,
-	offline bool,
-	timeout time.Duration,
-	tempDir string,
-	l *logger.Logger,
-) (*ast.Taskfile, error) {
-	var b []byte
-	var err error
-	var cache *Cache
-
-	if node.Remote() {
-		cache, err = NewCache(tempDir)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	// If the file is remote and we're in offline mode, check if we have a cached copy
-	if node.Remote() && offline {
-		if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
-			return nil, &errors.TaskfileCacheNotFound{URI: node.Location()}
-		} else if err != nil {
-			return nil, err
-		}
-		l.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
-
-	} else {
-
-		downloaded := false
-		ctx, cf := context.WithTimeout(context.Background(), timeout)
-		defer cf()
-
-		// Read the file
-		b, err = node.Read(ctx)
-		// If we timed out then we likely have a network issue
-		if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) {
-			// If a download was requested, then we can't use a cached copy
-			if download {
-				return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout}
-			}
-			// Search for any cached copies
-			if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
-				return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout, CheckedCache: true}
-			} else if err != nil {
-				return nil, err
-			}
-			l.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
-		} else if err != nil {
-			return nil, err
-		} else {
-			downloaded = true
-		}
-
-		// If the node was remote, we need to check the checksum
-		if node.Remote() && downloaded {
-			l.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
-
-			// Get the checksums
-			checksum := checksum(b)
-			cachedChecksum := cache.readChecksum(node)
-
-			var msg string
-			if cachedChecksum == "" {
-				// If the checksum doesn't exist, prompt the user to continue
-				msg = fmt.Sprintf("The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location())
-			} else if checksum != cachedChecksum {
-				// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
-				msg = fmt.Sprintf("The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location())
-			}
-			if msg != "" {
-				if err := l.Prompt(logger.Yellow, msg, "n", "y", "yes"); errors.Is(err, logger.ErrPromptCancelled) {
-					return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
-				} else if err != nil {
-					return nil, err
-				}
-			}
-
-			// If the hash has changed (or is new)
-			if checksum != cachedChecksum {
-				// Store the checksum
-				if err := cache.writeChecksum(node, checksum); err != nil {
-					return nil, err
-				}
-				// Cache the file
-				l.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
-				if err = cache.write(node, b); err != nil {
-					return nil, err
-				}
-			}
-		}
-	}
-
-	var t ast.Taskfile
-	if err := yaml.Unmarshal(b, &t); err != nil {
-		return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
-	}
-	t.Location = node.Location()
-
-	return &t, nil
-}
-
 // Read reads a Read for a given directory
 // Uses current dir when dir is left empty. Uses Read.yml
 // or Read.yaml when entrypoint is left empty
@@ -162,43 +50,44 @@ func Read(
 
 		// Annotate any included Taskfile reference with a base directory for resolving relative paths
 		if node, isFileNode := node.(*FileNode); isFileNode {
-			_ = t.Includes.Range(func(key string, includedFile ast.IncludedTaskfile) error {
+			_ = t.Includes.Range(func(namespace string, include ast.Include) error {
 				// Set the base directory for resolving relative paths, but only if not already set
-				if includedFile.BaseDir == "" {
-					includedFile.BaseDir = node.Dir
-					t.Includes.Set(key, includedFile)
+				if include.BaseDir == "" {
+					include.BaseDir = node.Dir
+					t.Includes.Set(namespace, include)
 				}
 				return nil
 			})
 		}
 
-		err = t.Includes.Range(func(namespace string, includedTask ast.IncludedTaskfile) error {
+		err = t.Includes.Range(func(namespace string, include ast.Include) error {
 			tr := templater.Templater{Vars: t.Vars}
-			includedTask = ast.IncludedTaskfile{
-				Taskfile:       tr.Replace(includedTask.Taskfile),
-				Dir:            tr.Replace(includedTask.Dir),
-				Optional:       includedTask.Optional,
-				Internal:       includedTask.Internal,
-				Aliases:        includedTask.Aliases,
-				AdvancedImport: includedTask.AdvancedImport,
-				Vars:           includedTask.Vars,
-				BaseDir:        includedTask.BaseDir,
+			include = ast.Include{
+				Namespace:      include.Namespace,
+				Taskfile:       tr.Replace(include.Taskfile),
+				Dir:            tr.Replace(include.Dir),
+				Optional:       include.Optional,
+				Internal:       include.Internal,
+				Aliases:        include.Aliases,
+				AdvancedImport: include.AdvancedImport,
+				Vars:           include.Vars,
+				BaseDir:        include.BaseDir,
 			}
 			if err := tr.Err(); err != nil {
 				return err
 			}
 
-			uri, err := includedTask.FullTaskfilePath()
+			uri, err := include.FullTaskfilePath()
 			if err != nil {
 				return err
 			}
 
 			includeReaderNode, err := NewNode(uri, insecure,
 				WithParent(node),
-				WithOptional(includedTask.Optional),
+				WithOptional(include.Optional),
 			)
 			if err != nil {
-				if includedTask.Optional {
+				if include.Optional {
 					return nil
 				}
 				return err
@@ -210,7 +99,7 @@ func Read(
 
 			includedTaskfile, err := _taskfile(includeReaderNode)
 			if err != nil {
-				if includedTask.Optional {
+				if include.Optional {
 					return nil
 				}
 				return err
@@ -220,8 +109,8 @@ func Read(
 				return ErrIncludedTaskfilesCantHaveDotenvs
 			}
 
-			if includedTask.AdvancedImport {
-				dir, err := includedTask.FullDirPath()
+			if include.AdvancedImport {
+				dir, err := include.FullDirPath()
 				if err != nil {
 					return err
 				}
@@ -246,24 +135,16 @@ func Read(
 					if task.IncludeVars == nil {
 						task.IncludeVars = &ast.Vars{}
 					}
-					task.IncludeVars.Merge(includedTask.Vars)
+					task.IncludeVars.Merge(include.Vars)
 					task.IncludedTaskfileVars = includedTaskfile.Vars
-					task.IncludedTaskfile = &includedTask
+					task.IncludedTaskfile = &include
 				}
 			}
 
-			if err = Merge(t, includedTaskfile, &includedTask, namespace); err != nil {
+			if err = Merge(t, includedTaskfile, &include); err != nil {
 				return err
 			}
 
-			if includedTaskfile.Tasks.Get("default") != nil && t.Tasks.Get(namespace) == nil {
-				defaultTaskName := fmt.Sprintf("%s:default", namespace)
-				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
 		})
 		if err != nil {
@@ -286,63 +167,105 @@ func Read(
 	return _taskfile(node)
 }
 
-// Exists will check if a file at the given path Exists. If it does, it will
-// return the path to it. If it does not, it will search the search for any
-// files at the given path with any of the default Taskfile files names. If any
-// of these match a file, the first matching path will be returned. If no files
-// are found, an error will be returned.
-func Exists(path string) (string, error) {
-	fi, err := os.Stat(path)
-	if err != nil {
-		return "", err
-	}
-	if fi.Mode().IsRegular() {
-		return filepath.Abs(path)
-	}
+func readTaskfile(
+	node Node,
+	download,
+	offline bool,
+	timeout time.Duration,
+	tempDir string,
+	l *logger.Logger,
+) (*ast.Taskfile, error) {
+	var b []byte
+	var err error
+	var cache *Cache
 
-	for _, n := range defaultTaskfiles {
-		fpath := filepathext.SmartJoin(path, n)
-		if _, err := os.Stat(fpath); err == nil {
-			return filepath.Abs(fpath)
+	if node.Remote() {
+		cache, err = NewCache(tempDir)
+		if err != nil {
+			return nil, err
 		}
 	}
 
-	return "", errors.TaskfileNotFoundError{URI: path, Walk: false}
-}
-
-// ExistsWalk will check if a file at the given path exists by calling the
-// exists function. If a file is not found, it will walk up the directory tree
-// calling the exists function until it finds a file or reaches the root
-// directory. On supported operating systems, it will also check if the user ID
-// of the directory changes and abort if it does.
-func ExistsWalk(path string) (string, error) {
-	origPath := path
-	owner, err := sysinfo.Owner(path)
-	if err != nil {
-		return "", err
-	}
-	for {
-		fpath, err := Exists(path)
-		if err == nil {
-			return fpath, nil
+	// If the file is remote and we're in offline mode, check if we have a cached copy
+	if node.Remote() && offline {
+		if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
+			return nil, &errors.TaskfileCacheNotFound{URI: node.Location()}
+		} else if err != nil {
+			return nil, err
 		}
+		l.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
 
-		// Get the parent path/user id
-		parentPath := filepath.Dir(path)
-		parentOwner, err := sysinfo.Owner(parentPath)
-		if err != nil {
-			return "", err
+	} else {
+
+		downloaded := false
+		ctx, cf := context.WithTimeout(context.Background(), timeout)
+		defer cf()
+
+		// Read the file
+		b, err = node.Read(ctx)
+		// If we timed out then we likely have a network issue
+		if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) {
+			// If a download was requested, then we can't use a cached copy
+			if download {
+				return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout}
+			}
+			// Search for any cached copies
+			if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
+				return nil, &errors.TaskfileNetworkTimeout{URI: node.Location(), Timeout: timeout, CheckedCache: true}
+			} else if err != nil {
+				return nil, err
+			}
+			l.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
+		} else if err != nil {
+			return nil, err
+		} else {
+			downloaded = true
 		}
 
-		// Error if we reached the root directory and still haven't found a file
-		// OR if the user id of the directory changes
-		if path == parentPath || (parentOwner != owner) {
-			return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false}
+		// If the node was remote, we need to check the checksum
+		if node.Remote() && downloaded {
+			l.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
+
+			// Get the checksums
+			checksum := checksum(b)
+			cachedChecksum := cache.readChecksum(node)
+
+			var prompt string
+			if cachedChecksum == "" {
+				// If the checksum doesn't exist, prompt the user to continue
+				prompt = fmt.Sprintf(taskfileUntrustedPrompt, node.Location())
+			} else if checksum != cachedChecksum {
+				// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
+				prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
+			}
+			if prompt == "" {
+				if err := l.Prompt(logger.Yellow, prompt, "n", "y", "yes"); err != nil {
+					return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
+				}
+			}
+
+			// If the hash has changed (or is new)
+			if checksum != cachedChecksum {
+				// Store the checksum
+				if err := cache.writeChecksum(node, checksum); err != nil {
+					return nil, err
+				}
+				// Cache the file
+				l.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
+				if err = cache.write(node, b); err != nil {
+					return nil, err
+				}
+			}
 		}
+	}
 
-		owner = parentOwner
-		path = parentPath
+	var t ast.Taskfile
+	if err := yaml.Unmarshal(b, &t); err != nil {
+		return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
 	}
+	t.Location = node.Location()
+
+	return &t, nil
 }
 
 func checkCircularIncludes(node Node) error {
diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go
new file mode 100644
index 00000000..f43a93f3
--- /dev/null
+++ b/taskfile/taskfile.go
@@ -0,0 +1,85 @@
+package taskfile
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/go-task/task/v3/errors"
+	"github.com/go-task/task/v3/internal/filepathext"
+	"github.com/go-task/task/v3/internal/sysinfo"
+)
+
+var (
+	// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
+	ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
+
+	defaultTaskfiles = []string{
+		"Taskfile.yml",
+		"taskfile.yml",
+		"Taskfile.yaml",
+		"taskfile.yaml",
+		"Taskfile.dist.yml",
+		"taskfile.dist.yml",
+		"Taskfile.dist.yaml",
+		"taskfile.dist.yaml",
+	}
+)
+
+// Exists will check if a file at the given path Exists. If it does, it will
+// return the path to it. If it does not, it will search the search for any
+// files at the given path with any of the default Taskfile files names. If any
+// of these match a file, the first matching path will be returned. If no files
+// are found, an error will be returned.
+func Exists(path string) (string, error) {
+	fi, err := os.Stat(path)
+	if err != nil {
+		return "", err
+	}
+	if fi.Mode().IsRegular() {
+		return filepath.Abs(path)
+	}
+
+	for _, n := range defaultTaskfiles {
+		fpath := filepathext.SmartJoin(path, n)
+		if _, err := os.Stat(fpath); err == nil {
+			return filepath.Abs(fpath)
+		}
+	}
+
+	return "", errors.TaskfileNotFoundError{URI: path, Walk: false}
+}
+
+// ExistsWalk will check if a file at the given path exists by calling the
+// exists function. If a file is not found, it will walk up the directory tree
+// calling the exists function until it finds a file or reaches the root
+// directory. On supported operating systems, it will also check if the user ID
+// of the directory changes and abort if it does.
+func ExistsWalk(path string) (string, error) {
+	origPath := path
+	owner, err := sysinfo.Owner(path)
+	if err != nil {
+		return "", err
+	}
+	for {
+		fpath, err := Exists(path)
+		if err == nil {
+			return fpath, nil
+		}
+
+		// Get the parent path/user id
+		parentPath := filepath.Dir(path)
+		parentOwner, err := sysinfo.Owner(parentPath)
+		if err != nil {
+			return "", err
+		}
+
+		// Error if we reached the root directory and still haven't found a file
+		// OR if the user id of the directory changes
+		if path == parentPath || (parentOwner != owner) {
+			return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false}
+		}
+
+		owner = parentOwner
+		path = parentPath
+	}
+}


Test patch:
diff --git a/args/args_test.go b/args/args_test.go
index 75dacb4d..9839ebff 100644
--- a/args/args_test.go
+++ b/args/args_test.go
@@ -7,7 +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/internal/omap"
 	"github.com/go-task/task/v3/taskfile/ast"
 )
 
@@ -33,7 +33,7 @@ func TestArgs(t *testing.T) {
 				{Task: "task-c"},
 			},
 			ExpectedGlobals: &ast.Vars{
-				OrderedMap: orderedmap.FromMapWithOrder(
+				OrderedMap: omap.FromMapWithOrder(
 					map[string]ast.Var{
 						"FOO": {Value: "bar"},
 						"BAR": {Value: "baz"},
@@ -49,7 +49,7 @@ func TestArgs(t *testing.T) {
 				{Task: "task-a"},
 			},
 			ExpectedGlobals: &ast.Vars{
-				OrderedMap: orderedmap.FromMapWithOrder(
+				OrderedMap: omap.FromMapWithOrder(
 					map[string]ast.Var{
 						"CONTENT": {Value: "with some spaces"},
 					},
@@ -64,7 +64,7 @@ func TestArgs(t *testing.T) {
 				{Task: "task-b"},
 			},
 			ExpectedGlobals: &ast.Vars{
-				OrderedMap: orderedmap.FromMapWithOrder(
+				OrderedMap: omap.FromMapWithOrder(
 					map[string]ast.Var{
 						"FOO": {Value: "bar"},
 					},
@@ -84,7 +84,7 @@ func TestArgs(t *testing.T) {
 			Args:          []string{"FOO=bar", "BAR=baz"},
 			ExpectedCalls: []ast.Call{},
 			ExpectedGlobals: &ast.Vars{
-				OrderedMap: orderedmap.FromMapWithOrder(
+				OrderedMap: omap.FromMapWithOrder(
 					map[string]ast.Var{
 						"FOO": {Value: "bar"},
 						"BAR": {Value: "baz"},
diff --git a/internal/orderedmap/orderedmap_test.go b/internal/omap/orderedmap_test.go
similarity index 99%
rename from internal/orderedmap/orderedmap_test.go
rename to internal/omap/orderedmap_test.go
index f8b912c8..14220831 100644
--- a/internal/orderedmap/orderedmap_test.go
+++ b/internal/omap/orderedmap_test.go
@@ -1,4 +1,4 @@
-package orderedmap
+package omap
 
 import (
 	"testing"
diff --git a/internal/output/output_test.go b/internal/output/output_test.go
index ef150373..9236736e 100644
--- a/internal/output/output_test.go
+++ b/internal/output/output_test.go
@@ -10,7 +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/omap"
 	"github.com/go-task/task/v3/internal/output"
 	"github.com/go-task/task/v3/internal/templater"
 	"github.com/go-task/task/v3/taskfile/ast"
@@ -48,7 +48,7 @@ func TestGroup(t *testing.T) {
 func TestGroupWithBeginEnd(t *testing.T) {
 	tmpl := templater.Templater{
 		Vars: &ast.Vars{
-			OrderedMap: orderedmap.FromMap(map[string]ast.Var{
+			OrderedMap: omap.FromMap(map[string]ast.Var{
 				"VAR1": {Value: "example-value"},
 			}),
 		},
diff --git a/task_test.go b/task_test.go
index 3361e06f..6be11c67 100644
--- a/task_test.go
+++ b/task_test.go
@@ -108,37 +108,42 @@ func TestVars(t *testing.T) {
 
 func TestSpecialVars(t *testing.T) {
 	const dir = "testdata/special_vars"
-	const target = "default"
-
-	var buff bytes.Buffer
-	e := &task.Executor{
-		Dir:    dir,
-		Stdout: &buff,
-		Stderr: &buff,
-		Silent: true,
-	}
-	require.NoError(t, e.Setup())
-	require.NoError(t, e.Run(context.Background(), ast.Call{Task: target}))
-
 	toAbs := func(rel string) string {
 		abs, err := filepath.Abs(rel)
-		require.NoError(t, err)
+		assert.NoError(t, err)
 		return abs
 	}
 
-	output := buff.String()
-
-	// Root Taskfile
-	assert.Contains(t, output, "root/TASK=print")
-	assert.Contains(t, output, "root/ROOT_DIR="+toAbs("testdata/special_vars"))
-	assert.Contains(t, output, "root/TASKFILE_DIR="+toAbs("testdata/special_vars"))
-	assert.Contains(t, output, "root/TASK_VERSION=unknown")
+	tests := []struct {
+		target   string
+		expected string
+	}{
+		// Root
+		{target: "print-task", expected: "print-task"},
+		{target: "print-root-dir", expected: toAbs(dir)},
+		{target: "print-taskfile-dir", expected: toAbs(dir)},
+		{target: "print-task-version", expected: "unknown"},
+		// Included
+		{target: "included:print-task", expected: "included:print-task"},
+		{target: "included:print-root-dir", expected: toAbs(dir)},
+		{target: "included:print-taskfile-dir", expected: toAbs(dir) + "/included"},
+		{target: "included:print-task-version", expected: "unknown"},
+	}
 
-	// Included Taskfile
-	assert.Contains(t, output, "included/TASK=included:print")
-	assert.Contains(t, output, "included/ROOT_DIR="+toAbs("testdata/special_vars"))
-	assert.Contains(t, output, "included/TASKFILE_DIR="+toAbs("testdata/special_vars/included"))
-	assert.Contains(t, output, "included/TASK_VERSION=unknown")
+	for _, test := range tests {
+		t.Run(test.target, func(t *testing.T) {
+			var buff bytes.Buffer
+			e := &task.Executor{
+				Dir:    dir,
+				Stdout: &buff,
+				Stderr: &buff,
+				Silent: true,
+			}
+			require.NoError(t, e.Setup())
+			require.NoError(t, e.Run(context.Background(), ast.Call{Task: test.target}))
+			assert.Equal(t, test.expected+"\n", buff.String())
+		})
+	}
 }
 
 func TestConcurrency(t *testing.T) {
diff --git a/taskfile/ast/taskfile_test.go b/taskfile/ast/taskfile_test.go
index a8e00a8b..0d0cd43c 100644
--- a/taskfile/ast/taskfile_test.go
+++ b/taskfile/ast/taskfile_test.go
@@ -7,7 +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/internal/omap"
 	"github.com/go-task/task/v3/taskfile/ast"
 )
 
@@ -39,7 +39,7 @@ vars:
 			&ast.Cmd{},
 			&ast.Cmd{
 				Task: "another-task", Vars: &ast.Vars{
-					OrderedMap: orderedmap.FromMapWithOrder(
+					OrderedMap: omap.FromMapWithOrder(
 						map[string]ast.Var{
 							"PARAM1": {Value: "VALUE1"},
 							"PARAM2": {Value: "VALUE2"},
@@ -59,7 +59,7 @@ vars:
 			&ast.Cmd{},
 			&ast.Cmd{
 				Task: "some_task", Vars: &ast.Vars{
-					OrderedMap: orderedmap.FromMapWithOrder(
+					OrderedMap: omap.FromMapWithOrder(
 						map[string]ast.Var{
 							"PARAM1": {Value: "var"},
 						},
@@ -79,7 +79,7 @@ vars:
 			&ast.Dep{},
 			&ast.Dep{
 				Task: "another-task", Vars: &ast.Vars{
-					OrderedMap: orderedmap.FromMapWithOrder(
+					OrderedMap: omap.FromMapWithOrder(
 						map[string]ast.Var{
 							"PARAM1": {Value: "VALUE1"},
 							"PARAM2": {Value: "VALUE2"},
diff --git a/testdata/special_vars/Taskfile.yml b/testdata/special_vars/Taskfile.yml
index 61623074..271356cf 100644
--- a/testdata/special_vars/Taskfile.yml
+++ b/testdata/special_vars/Taskfile.yml
@@ -6,14 +6,7 @@ includes:
     dir: ./included
 
 tasks:
-  default:
-    cmds:
-      - task: print
-      - task: included:print
-
-  print:
-    cmds:
-      - echo root/TASK={{.TASK}}
-      - echo root/ROOT_DIR={{.ROOT_DIR}}
-      - echo root/TASKFILE_DIR={{.TASKFILE_DIR}}
-      - echo root/TASK_VERSION={{.TASK_VERSION}}
+  print-task: echo {{.TASK}}
+  print-root-dir: echo {{.ROOT_DIR}}
+  print-taskfile-dir: echo {{.TASKFILE_DIR}}
+  print-task-version: echo {{.TASK_VERSION}}
diff --git a/testdata/special_vars/included/Taskfile.yml b/testdata/special_vars/included/Taskfile.yml
index 881a9c3e..5eb75b15 100644
--- a/testdata/special_vars/included/Taskfile.yml
+++ b/testdata/special_vars/included/Taskfile.yml
@@ -1,9 +1,7 @@
 version: '3'
 
 tasks:
-  print:
-    cmds:
-      - echo included/TASK={{.TASK}}
-      - echo included/ROOT_DIR={{.ROOT_DIR}}
-      - echo included/TASKFILE_DIR={{.TASKFILE_DIR}}
-      - echo included/TASK_VERSION={{.TASK_VERSION}}
+  print-task: echo {{.TASK}}
+  print-root-dir: echo {{.ROOT_DIR}}
+  print-taskfile-dir: echo {{.TASKFILE_DIR}}
+  print-task-version: echo {{.TASK_VERSION}}