mirror of
https://github.com/go-task/task.git
synced 2025-04-13 11:50:50 +02:00
Merge pull request #656 from tylermmorton/master
Add support for multi-level includes
This commit is contained in:
commit
41cd7acc87
@ -164,10 +164,6 @@ includes:
|
||||
> The included Taskfiles must be using the same schema version the main
|
||||
> Taskfile uses.
|
||||
|
||||
> Also, for now included Taskfiles can't include other Taskfiles.
|
||||
> This was a deliberate decision to keep use and implementation simple.
|
||||
> If you disagree, open an GitHub issue and explain your use case. =)
|
||||
|
||||
### Optional includes
|
||||
|
||||
Includes marked as optional will allow Task to continue execution as normal if
|
||||
|
7
task.go
7
task.go
@ -107,7 +107,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
|
||||
// Setup setups Executor's internal state
|
||||
func (e *Executor) Setup() error {
|
||||
var err error
|
||||
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
|
||||
e.Taskfile, err = read.Taskfile(&read.ReaderNode{
|
||||
Dir: e.Dir,
|
||||
Entrypoint: e.Entrypoint,
|
||||
Parent: nil,
|
||||
Optional: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
29
task_test.go
29
task_test.go
@ -753,6 +753,35 @@ func TestIncludes(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludesMultiLevel(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes_multi_level",
|
||||
Target: "default",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"called_one.txt": "one",
|
||||
"called_two.txt": "two",
|
||||
"called_three.txt": "three",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludeCycle(t *testing.T) {
|
||||
const dir = "testdata/includes_cycle"
|
||||
expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
|
||||
assert.EqualError(t, e.Setup(), expectedError)
|
||||
}
|
||||
|
||||
func TestIncorrectVersionIncludes(t *testing.T) {
|
||||
const dir = "testdata/incorrect_includes"
|
||||
expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
|
||||
|
@ -15,8 +15,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
|
||||
ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
|
||||
// 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")
|
||||
|
||||
@ -28,21 +26,29 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type ReaderNode struct {
|
||||
Dir string
|
||||
Entrypoint string
|
||||
Optional bool
|
||||
Parent *ReaderNode
|
||||
}
|
||||
|
||||
// Taskfile reads a Taskfile for a given directory
|
||||
// Uses current dir when dir is left empty. Uses Taskfile.yml
|
||||
// or Taskfile.yaml when entrypoint is left empty
|
||||
func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
if dir == "" {
|
||||
func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
||||
if readerNode.Dir == "" {
|
||||
d, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir = d
|
||||
readerNode.Dir = d
|
||||
}
|
||||
path, err := exists(filepath.Join(dir, entrypoint))
|
||||
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readerNode.Entrypoint = filepath.Base(path)
|
||||
|
||||
t, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
@ -74,9 +80,8 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
return err
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(dir, path)
|
||||
path = filepath.Join(readerNode.Dir, path)
|
||||
}
|
||||
|
||||
path, err = exists(path)
|
||||
if err != nil {
|
||||
if includedTask.Optional {
|
||||
@ -85,12 +90,23 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
includedTaskfile, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
includeReaderNode := &ReaderNode{
|
||||
Dir: filepath.Dir(path),
|
||||
Entrypoint: filepath.Base(path),
|
||||
Parent: readerNode,
|
||||
Optional: includedTask.Optional,
|
||||
}
|
||||
|
||||
if err := checkCircularIncludes(includeReaderNode); err != nil {
|
||||
return err
|
||||
}
|
||||
if includedTaskfile.Includes.Len() > 0 {
|
||||
return ErrIncludedTaskfilesCantHaveIncludes
|
||||
|
||||
includedTaskfile, err := Taskfile(includeReaderNode)
|
||||
if err != nil {
|
||||
if includedTask.Optional {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
|
||||
@ -100,12 +116,12 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
if includedTask.AdvancedImport {
|
||||
for k, v := range includedTaskfile.Vars.Mapping {
|
||||
o := v
|
||||
o.Dir = filepath.Join(dir, includedTask.Dir)
|
||||
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
|
||||
includedTaskfile.Vars.Mapping[k] = o
|
||||
}
|
||||
for k, v := range includedTaskfile.Env.Mapping {
|
||||
o := v
|
||||
o.Dir = filepath.Join(dir, includedTask.Dir)
|
||||
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
|
||||
includedTaskfile.Env.Mapping[k] = o
|
||||
}
|
||||
|
||||
@ -128,7 +144,7 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
}
|
||||
|
||||
if v < 3.0 {
|
||||
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
|
||||
path = filepath.Join(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
osTaskfile, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
@ -178,3 +194,25 @@ func exists(path string) (string, error) {
|
||||
|
||||
return "", fmt.Errorf(`task: No Taskfile found in "%s". Use "task --init" to create a new one`, path)
|
||||
}
|
||||
|
||||
func checkCircularIncludes(node *ReaderNode) error {
|
||||
if node == nil {
|
||||
return errors.New("task: failed to check for include cycle: node was nil")
|
||||
}
|
||||
if node.Parent == nil {
|
||||
return errors.New("task: failed to check for include cycle: node.Parent was nil")
|
||||
}
|
||||
var curNode = node
|
||||
var basePath = filepath.Join(node.Dir, node.Entrypoint)
|
||||
for curNode.Parent != nil {
|
||||
curNode = curNode.Parent
|
||||
curPath := filepath.Join(curNode.Dir, curNode.Entrypoint)
|
||||
if curPath == basePath {
|
||||
return fmt.Errorf("task: include cycle detected between %s <--> %s",
|
||||
curPath,
|
||||
filepath.Join(node.Parent.Dir, node.Parent.Entrypoint),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
12
testdata/includes_cycle/Taskfile.yml
vendored
Normal file
12
testdata/includes_cycle/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
'one': ./one/Taskfile.yml
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "called_dep" > called_dep.txt
|
||||
level1:
|
||||
cmds:
|
||||
- echo "hello level 1"
|
9
testdata/includes_cycle/one/Taskfile.yml
vendored
Normal file
9
testdata/includes_cycle/one/Taskfile.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
'two': ./two/Taskfile.yml
|
||||
|
||||
tasks:
|
||||
level2:
|
||||
cmds:
|
||||
- echo "hello level 2"
|
9
testdata/includes_cycle/one/two/Taskfile.yml
vendored
Normal file
9
testdata/includes_cycle/one/two/Taskfile.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
bad: "../../Taskfile.yml"
|
||||
|
||||
tasks:
|
||||
level3:
|
||||
cmds:
|
||||
- echo "hello level 3"
|
11
testdata/includes_multi_level/Taskfile.yml
vendored
Normal file
11
testdata/includes_multi_level/Taskfile.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
'one': ./one/
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: one:default
|
||||
- task: one:two:default
|
||||
- task: one:two:three:default
|
1
testdata/includes_multi_level/called_one.txt
vendored
Normal file
1
testdata/includes_multi_level/called_one.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
one
|
1
testdata/includes_multi_level/called_three.txt
vendored
Normal file
1
testdata/includes_multi_level/called_three.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
three
|
1
testdata/includes_multi_level/called_two.txt
vendored
Normal file
1
testdata/includes_multi_level/called_two.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
two
|
8
testdata/includes_multi_level/one/Taskfile.yml
vendored
Normal file
8
testdata/includes_multi_level/one/Taskfile.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
'two': ./two/
|
||||
|
||||
tasks:
|
||||
default: echo one > called_one.txt
|
||||
|
7
testdata/includes_multi_level/one/two/Taskfile.yml
vendored
Normal file
7
testdata/includes_multi_level/one/two/Taskfile.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
'three': ./three/Taskfile.yml
|
||||
|
||||
tasks:
|
||||
default: echo two > called_two.txt
|
5
testdata/includes_multi_level/one/two/three/Taskfile.yml
vendored
Normal file
5
testdata/includes_multi_level/one/two/three/Taskfile.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default: echo three > called_three.txt
|
||||
|
Loading…
x
Reference in New Issue
Block a user