mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	feat: root remote taskfiles
This commit is contained in:
		| @@ -5,7 +5,6 @@ import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/spf13/pflag" | ||||
| @@ -94,11 +93,6 @@ func run() error { | ||||
| 		dir = home | ||||
| 	} | ||||
|  | ||||
| 	if entrypoint != "" { | ||||
| 		dir = filepath.Dir(entrypoint) | ||||
| 		entrypoint = filepath.Base(entrypoint) | ||||
| 	} | ||||
|  | ||||
| 	var taskSorter sort.TaskSorter | ||||
| 	switch flags.TaskSort { | ||||
| 	case "none": | ||||
|   | ||||
| @@ -131,10 +131,6 @@ func Validate() error { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if Dir != "" && Entrypoint != "" { | ||||
| 		return errors.New("task: You can't set both --dir and --taskfile") | ||||
| 	} | ||||
|  | ||||
| 	if Output.Name != "group" { | ||||
| 		if Output.Group.Begin != "" { | ||||
| 			return errors.New("task: You can't set --output-group-begin without --output=group") | ||||
|   | ||||
							
								
								
									
										4
									
								
								setup.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								setup.go
									
									
									
									
									
								
							| @@ -54,11 +54,11 @@ func (e *Executor) Setup() error { | ||||
| } | ||||
|  | ||||
| func (e *Executor) getRootNode() (taskfile.Node, error) { | ||||
| 	node, err := taskfile.NewRootNode(e.Dir, e.Entrypoint, e.Insecure) | ||||
| 	node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	e.Dir = node.BaseDir() | ||||
| 	e.Dir = node.Dir() | ||||
| 	return node, err | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								task_test.go
									
									
									
									
									
								
							| @@ -73,7 +73,7 @@ func (fct fileContentTest) Run(t *testing.T) { | ||||
|  | ||||
| 	for name, expectContent := range fct.Files { | ||||
| 		t.Run(fct.name(name), func(t *testing.T) { | ||||
| 			path := filepathext.SmartJoin(fct.Dir, name) | ||||
| 			path := filepathext.SmartJoin(e.Dir, name) | ||||
| 			b, err := os.ReadFile(path) | ||||
| 			require.NoError(t, err, "Error reading file") | ||||
| 			s := string(b) | ||||
| @@ -1123,8 +1123,8 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) { | ||||
|  | ||||
| func TestIncludesFromCustomTaskfile(t *testing.T) { | ||||
| 	tt := fileContentTest{ | ||||
| 		Entrypoint: "testdata/includes_yaml/Custom.ext", | ||||
| 		Dir:        "testdata/includes_yaml", | ||||
| 		Entrypoint: "Custom.ext", | ||||
| 		Target:     "default", | ||||
| 		TrimSpace:  true, | ||||
| 		Files: map[string]string{ | ||||
| @@ -1486,16 +1486,12 @@ func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) { | ||||
| 	const dir = "testdata/dotenv/error_included_envs" | ||||
| 	const entry = "Taskfile.yml" | ||||
|  | ||||
| 	var buff bytes.Buffer | ||||
| 	e := task.Executor{ | ||||
| 		Dir:        dir, | ||||
| 		Entrypoint: entry, | ||||
| 		Summary:    true, | ||||
| 		Stdout:     &buff, | ||||
| 		Stderr:     &buff, | ||||
| 		Dir:     "testdata/dotenv/error_included_envs", | ||||
| 		Summary: true, | ||||
| 		Stdout:  &buff, | ||||
| 		Stderr:  &buff, | ||||
| 	} | ||||
|  | ||||
| 	err := e.Setup() | ||||
|   | ||||
| @@ -2,13 +2,9 @@ package ast | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"gopkg.in/yaml.v3" | ||||
|  | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| @@ -22,7 +18,6 @@ type Include struct { | ||||
| 	Aliases        []string | ||||
| 	AdvancedImport bool | ||||
| 	Vars           *Vars | ||||
| 	BaseDir        string // The directory from which the including taskfile was loaded; used to resolve relative paths | ||||
| } | ||||
|  | ||||
| // Includes represents information about included tasksfiles | ||||
| @@ -120,39 +115,5 @@ func (include *Include) DeepCopy() *Include { | ||||
| 		Internal:       include.Internal, | ||||
| 		AdvancedImport: include.AdvancedImport, | ||||
| 		Vars:           include.Vars.DeepCopy(), | ||||
| 		BaseDir:        include.BaseDir, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // FullTaskfilePath returns the fully qualified path to the included 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 (include *Include) FullDirPath() (string, error) { | ||||
| 	return include.resolvePath(include.Dir) | ||||
| } | ||||
|  | ||||
| func (include *Include) resolvePath(path string) (string, error) { | ||||
| 	// If the file is remote, we don't need to resolve the path | ||||
| 	if strings.Contains(include.Taskfile, "://") { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	path, err := execext.Expand(path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	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, include.BaseDir, err) | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|   | ||||
| @@ -8,20 +8,25 @@ import ( | ||||
|  | ||||
| 	"github.com/go-task/task/v3/errors" | ||||
| 	"github.com/go-task/task/v3/internal/experiments" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile/ast" | ||||
| ) | ||||
|  | ||||
| type Node interface { | ||||
| 	Read(ctx context.Context) ([]byte, error) | ||||
| 	Parent() Node | ||||
| 	Location() string | ||||
| 	Dir() string | ||||
| 	Optional() bool | ||||
| 	Remote() bool | ||||
| 	BaseDir() string | ||||
| 	ResolveIncludeEntrypoint(include ast.Include) (string, error) | ||||
| 	ResolveIncludeDir(include ast.Include) (string, error) | ||||
| } | ||||
|  | ||||
| func NewRootNode( | ||||
| 	dir string, | ||||
| 	l *logger.Logger, | ||||
| 	entrypoint string, | ||||
| 	dir string, | ||||
| 	insecure bool, | ||||
| ) (Node, error) { | ||||
| 	dir = getDefaultDir(entrypoint, dir) | ||||
| @@ -30,32 +35,24 @@ func NewRootNode( | ||||
| 	if (stat.Mode()&os.ModeCharDevice) == 0 && stat.Size() > 0 { | ||||
| 		return NewStdinNode(dir) | ||||
| 	} | ||||
| 	// If no entrypoint is specified, search for a taskfile | ||||
| 	if entrypoint == "" { | ||||
| 		root, err := ExistsWalk(dir) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return NewNode(root, insecure) | ||||
| 	} | ||||
| 	// Use the specified entrypoint | ||||
| 	uri := filepath.Join(dir, entrypoint) | ||||
| 	return NewNode(uri, insecure) | ||||
| 	return NewNode(l, entrypoint, dir, insecure) | ||||
| } | ||||
|  | ||||
| func NewNode( | ||||
| 	uri string, | ||||
| 	l *logger.Logger, | ||||
| 	entrypoint string, | ||||
| 	dir string, | ||||
| 	insecure bool, | ||||
| 	opts ...NodeOption, | ||||
| ) (Node, error) { | ||||
| 	var node Node | ||||
| 	var err error | ||||
| 	switch getScheme(uri) { | ||||
| 	switch getScheme(entrypoint) { | ||||
| 	case "http", "https": | ||||
| 		node, err = NewHTTPNode(uri, insecure, opts...) | ||||
| 		node, err = NewHTTPNode(l, entrypoint, dir, insecure, opts...) | ||||
| 	default: | ||||
| 		// If no other scheme matches, we assume it's a file | ||||
| 		node, err = NewFileNode(uri, opts...) | ||||
| 		node, err = NewFileNode(l, entrypoint, dir, opts...) | ||||
| 	} | ||||
| 	if node.Remote() && !experiments.RemoteTaskfiles.Enabled { | ||||
| 		return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles") | ||||
|   | ||||
| @@ -9,6 +9,7 @@ type ( | ||||
| 	BaseNode struct { | ||||
| 		parent   Node | ||||
| 		optional bool | ||||
| 		dir      string | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| @@ -16,6 +17,7 @@ func NewBaseNode(opts ...NodeOption) *BaseNode { | ||||
| 	node := &BaseNode{ | ||||
| 		parent:   nil, | ||||
| 		optional: false, | ||||
| 		dir:      "", | ||||
| 	} | ||||
|  | ||||
| 	// Apply options | ||||
| @@ -45,3 +47,7 @@ func WithOptional(optional bool) NodeOption { | ||||
| func (node *BaseNode) Optional() bool { | ||||
| 	return node.optional | ||||
| } | ||||
|  | ||||
| func (node *BaseNode) Dir() string { | ||||
| 	return node.dir | ||||
| } | ||||
|   | ||||
| @@ -5,39 +5,36 @@ import ( | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/filepathext" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile/ast" | ||||
| ) | ||||
|  | ||||
| // A FileNode is a node that reads a taskfile from the local filesystem. | ||||
| type FileNode struct { | ||||
| 	*BaseNode | ||||
| 	Dir        string | ||||
| 	Entrypoint string | ||||
| } | ||||
|  | ||||
| func NewFileNode(uri string, opts ...NodeOption) (*FileNode, error) { | ||||
| func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { | ||||
| 	var err error | ||||
| 	base := NewBaseNode(opts...) | ||||
| 	if uri == "" { | ||||
| 		d, err := os.Getwd() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		uri = d | ||||
| 	} | ||||
| 	path, err := Exists(uri) | ||||
| 	entrypoint, dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, dir) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	base.dir = dir | ||||
| 	return &FileNode{ | ||||
| 		BaseNode:   base, | ||||
| 		Dir:        filepath.Dir(path), | ||||
| 		Entrypoint: filepath.Base(path), | ||||
| 		Entrypoint: entrypoint, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (node *FileNode) Location() string { | ||||
| 	return filepathext.SmartJoin(node.Dir, node.Entrypoint) | ||||
| 	return node.Entrypoint | ||||
| } | ||||
|  | ||||
| func (node *FileNode) Remote() bool { | ||||
| @@ -53,6 +50,67 @@ func (node *FileNode) Read(ctx context.Context) ([]byte, error) { | ||||
| 	return io.ReadAll(f) | ||||
| } | ||||
|  | ||||
| func (node *FileNode) BaseDir() string { | ||||
| 	return node.Dir | ||||
| // resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and | ||||
| // populates them with default values if necessary. | ||||
| func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (string, string, error) { | ||||
| 	var err error | ||||
| 	if entrypoint != "" { | ||||
| 		entrypoint, err = Exists(l, entrypoint) | ||||
| 		if err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 		if dir == "" { | ||||
| 			dir = filepath.Dir(entrypoint) | ||||
| 		} | ||||
| 		return entrypoint, dir, nil | ||||
| 	} | ||||
| 	if dir == "" { | ||||
| 		dir, err = os.Getwd() | ||||
| 		if err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 	} | ||||
| 	entrypoint, err = ExistsWalk(l, dir) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	dir = filepath.Dir(entrypoint) | ||||
| 	return entrypoint, dir, nil | ||||
| } | ||||
|  | ||||
| func (node *FileNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) { | ||||
| 	// If the file is remote, we don't need to resolve the path | ||||
| 	if strings.Contains(include.Taskfile, "://") { | ||||
| 		return include.Taskfile, nil | ||||
| 	} | ||||
|  | ||||
| 	path, err := execext.Expand(include.Taskfile) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory | ||||
| 	// This means that files are included relative to one another | ||||
| 	entrypointDir := filepath.Dir(node.Entrypoint) | ||||
| 	return filepathext.SmartJoin(entrypointDir, path), nil | ||||
| } | ||||
|  | ||||
| func (node *FileNode) ResolveIncludeDir(include ast.Include) (string, error) { | ||||
| 	path, err := execext.Expand(include.Dir) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory | ||||
| 	// This means that files are included relative to one another | ||||
| 	entrypointDir := filepath.Dir(node.Entrypoint) | ||||
| 	return filepathext.SmartJoin(entrypointDir, path), nil | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,13 @@ import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/errors" | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/filepathext" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile/ast" | ||||
| ) | ||||
|  | ||||
| // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. | ||||
| @@ -15,14 +20,19 @@ type HTTPNode struct { | ||||
| 	URL *url.URL | ||||
| } | ||||
|  | ||||
| func NewHTTPNode(uri string, insecure bool, opts ...NodeOption) (*HTTPNode, error) { | ||||
| func NewHTTPNode(l *logger.Logger, entrypoint, dir string, insecure bool, opts ...NodeOption) (*HTTPNode, error) { | ||||
| 	base := NewBaseNode(opts...) | ||||
| 	url, err := url.Parse(uri) | ||||
| 	base.dir = dir | ||||
| 	url, err := url.Parse(entrypoint) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if url.Scheme == "http" && !insecure { | ||||
| 		return nil, &errors.TaskfileNotSecureError{URI: uri} | ||||
| 		return nil, &errors.TaskfileNotSecureError{URI: entrypoint} | ||||
| 	} | ||||
| 	url, err = RemoteExists(l, url) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &HTTPNode{ | ||||
| 		BaseNode: base, | ||||
| @@ -66,6 +76,26 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) { | ||||
| 	return b, nil | ||||
| } | ||||
|  | ||||
| func (node *HTTPNode) BaseDir() string { | ||||
| 	return "" | ||||
| func (node *HTTPNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) { | ||||
| 	ref, err := url.Parse(include.Taskfile) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return node.URL.ResolveReference(ref).String(), nil | ||||
| } | ||||
|  | ||||
| func (node *HTTPNode) ResolveIncludeDir(include ast.Include) (string, error) { | ||||
| 	path, err := execext.Expand(include.Dir) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory | ||||
| 	// This means that files are included relative to one another | ||||
| 	entrypointDir := filepath.Dir(node.Dir()) | ||||
| 	return filepathext.SmartJoin(entrypointDir, path), nil | ||||
| } | ||||
|   | ||||
| @@ -5,19 +5,23 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/filepathext" | ||||
| 	"github.com/go-task/task/v3/taskfile/ast" | ||||
| ) | ||||
|  | ||||
| // A StdinNode is a node that reads a taskfile from the standard input stream. | ||||
| type StdinNode struct { | ||||
| 	*BaseNode | ||||
| 	Dir string | ||||
| } | ||||
|  | ||||
| func NewStdinNode(dir string) (*StdinNode, error) { | ||||
| 	base := NewBaseNode() | ||||
| 	base.dir = dir | ||||
| 	return &StdinNode{ | ||||
| 		BaseNode: base, | ||||
| 		Dir:      dir, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| @@ -41,6 +45,33 @@ func (node *StdinNode) Read(ctx context.Context) ([]byte, error) { | ||||
| 	return stdin, nil | ||||
| } | ||||
|  | ||||
| func (node *StdinNode) BaseDir() string { | ||||
| 	return node.Dir | ||||
| func (node *StdinNode) ResolveIncludeEntrypoint(include ast.Include) (string, error) { | ||||
| 	// If the file is remote, we don't need to resolve the path | ||||
| 	if strings.Contains(include.Taskfile, "://") { | ||||
| 		return include.Taskfile, nil | ||||
| 	} | ||||
|  | ||||
| 	path, err := execext.Expand(include.Taskfile) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	return filepathext.SmartJoin(node.Dir(), path), nil | ||||
| } | ||||
|  | ||||
| func (node *StdinNode) ResolveIncludeDir(include ast.Include) (string, error) { | ||||
| 	path, err := execext.Expand(include.Dir) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if filepathext.IsAbs(path) { | ||||
| 		return path, nil | ||||
| 	} | ||||
|  | ||||
| 	return filepathext.SmartJoin(node.Dir(), path), nil | ||||
| } | ||||
|   | ||||
| @@ -48,17 +48,6 @@ func Read( | ||||
| 			return nil, &errors.TaskfileVersionCheckError{URI: node.Location()} | ||||
| 		} | ||||
|  | ||||
| 		if dir := node.BaseDir(); dir != "" { | ||||
| 			_ = tf.Includes.Range(func(namespace string, include ast.Include) error { | ||||
| 				// Set the base directory for resolving relative paths, but only if not already set | ||||
| 				if include.BaseDir == "" { | ||||
| 					include.BaseDir = dir | ||||
| 					tf.Includes.Set(namespace, include) | ||||
| 				} | ||||
| 				return nil | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		err = tf.Includes.Range(func(namespace string, include ast.Include) error { | ||||
| 			cache := &templater.Cache{Vars: tf.Vars} | ||||
| 			include = ast.Include{ | ||||
| @@ -70,18 +59,22 @@ func Read( | ||||
| 				Aliases:        include.Aliases, | ||||
| 				AdvancedImport: include.AdvancedImport, | ||||
| 				Vars:           include.Vars, | ||||
| 				BaseDir:        include.BaseDir, | ||||
| 			} | ||||
| 			if err := cache.Err(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			uri, err := include.FullTaskfilePath() | ||||
| 			entrypoint, err := node.ResolveIncludeEntrypoint(include) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			includeReaderNode, err := NewNode(uri, insecure, | ||||
| 			dir, err := node.ResolveIncludeDir(include) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			includeReaderNode, err := NewNode(l, entrypoint, dir, insecure, | ||||
| 				WithParent(node), | ||||
| 				WithOptional(include.Optional), | ||||
| 			) | ||||
| @@ -109,11 +102,6 @@ func Read( | ||||
| 			} | ||||
|  | ||||
| 			if include.AdvancedImport { | ||||
| 				dir, err := include.FullDirPath() | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
|  | ||||
| 				// nolint: errcheck | ||||
| 				includedTaskfile.Vars.Range(func(k string, v ast.Var) error { | ||||
| 					o := v | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| package taskfile | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| @@ -23,14 +28,80 @@ var ( | ||||
| 		"Taskfile.dist.yaml", | ||||
| 		"taskfile.dist.yaml", | ||||
| 	} | ||||
|  | ||||
| 	allowedContentTypes = []string{ | ||||
| 		"text/plain", | ||||
| 		"text/yaml", | ||||
| 		"text/x-yaml", | ||||
| 		"application/yaml", | ||||
| 		"application/x-yaml", | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // RemoteExists will check if a file at the given URL Exists. If it does, it | ||||
| // will return its URL. If it does not, it will search the search for any files | ||||
| // at the given URL 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 RemoteExists(l *logger.Logger, u *url.URL) (*url.URL, error) { | ||||
| 	// Create a new HEAD request for the given URL to check if the resource exists | ||||
| 	req, err := http.NewRequest("HEAD", u.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.TaskfileFetchFailedError{URI: u.String()} | ||||
| 	} | ||||
|  | ||||
| 	// Request the given URL | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.TaskfileFetchFailedError{URI: u.String()} | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	// If the request was successful and the content type is allowed, return the | ||||
| 	// URL The content type check is to avoid downloading files that are not | ||||
| 	// Taskfiles It means we can try other files instead of downloading | ||||
| 	// something that is definitely not a Taskfile | ||||
| 	contentType := resp.Header.Get("Content-Type") | ||||
| 	if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool { | ||||
| 		return strings.Contains(contentType, s) | ||||
| 	}) { | ||||
| 		return u, nil | ||||
| 	} | ||||
|  | ||||
| 	// If the request was not successful, append the default Taskfile names to | ||||
| 	// the URL and return the URL of the first successful request | ||||
| 	for _, taskfile := range defaultTaskfiles { | ||||
| 		// Fixes a bug with JoinPath where a leading slash is not added to the | ||||
| 		// path if it is empty | ||||
| 		if u.Path == "" { | ||||
| 			u.Path = "/" | ||||
| 		} | ||||
| 		alt := u.JoinPath(taskfile) | ||||
| 		req.URL = alt | ||||
|  | ||||
| 		// Try the alternative URL | ||||
| 		resp, err = http.DefaultClient.Do(req) | ||||
| 		if err != nil { | ||||
| 			return nil, errors.TaskfileFetchFailedError{URI: u.String()} | ||||
| 		} | ||||
| 		defer resp.Body.Close() | ||||
|  | ||||
| 		// If the request was successful, return the URL | ||||
| 		if resp.StatusCode == http.StatusOK { | ||||
| 			l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile) | ||||
| 			return alt, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false} | ||||
| } | ||||
|  | ||||
| // 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) { | ||||
| // return the path to it. If it does not, it will 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(l *logger.Logger, path string) (string, error) { | ||||
| 	fi, err := os.Stat(path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| @@ -42,10 +113,11 @@ func Exists(path string) (string, error) { | ||||
| 		return filepath.Abs(path) | ||||
| 	} | ||||
|  | ||||
| 	for _, n := range defaultTaskfiles { | ||||
| 		fpath := filepathext.SmartJoin(path, n) | ||||
| 		if _, err := os.Stat(fpath); err == nil { | ||||
| 			return filepath.Abs(fpath) | ||||
| 	for _, taskfile := range defaultTaskfiles { | ||||
| 		alt := filepathext.SmartJoin(path, taskfile) | ||||
| 		if _, err := os.Stat(alt); err == nil { | ||||
| 			l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile) | ||||
| 			return filepath.Abs(alt) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -57,14 +129,14 @@ func Exists(path string) (string, error) { | ||||
| // 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) { | ||||
| func ExistsWalk(l *logger.Logger, path string) (string, error) { | ||||
| 	origPath := path | ||||
| 	owner, err := sysinfo.Owner(path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	for { | ||||
| 		fpath, err := Exists(path) | ||||
| 		fpath, err := Exists(l, path) | ||||
| 		if err == nil { | ||||
| 			return fpath, nil | ||||
| 		} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user