Repository: go-task/task
Pull request #292 (2020-02-09 11:50:49)

Added option to make included Taskfile run commands on its own directory
Hi, this pull request relates to [#260](https://github.com/go-task/task/issues/260) and [#144](https://github.com/go-task/task/issues/144) issues. This syntax was implemented:

``` yaml
includes:
  docs:
    taskfile: ./docs/Taskfile.yml
    dir: ./docs
```

Also added support `Taskfile_{GOOS}.yaml` for included task files.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d81487a9..6ebab2b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
   ([#252](https://github.com/go-task/task/issues/252)).
 - Implement short task syntax
   ([#194](https://github.com/go-task/task/issues/194), [#240](https://github.com/go-task/task/pull/240)).
+- Added option to make included Taskfile run commands on its own directory
+  ([#260](https://github.com/go-task/task/issues/260), [#144](https://github.com/go-task/task/issues/144))
 
 # v3.0.0 - Preview 2
 
diff --git a/internal/taskfile/included_taskfile.go b/internal/taskfile/included_taskfile.go
new file mode 100644
index 00000000..346744f9
--- /dev/null
+++ b/internal/taskfile/included_taskfile.go
@@ -0,0 +1,40 @@
+package taskfile
+
+import "errors"
+
+var (
+	// ErrCantUnmarshalIncludedTaskfile is returned for invalid var YAML.
+	ErrCantUnmarshalIncludedTaskfile = errors.New("task: can't unmarshal included value")
+)
+
+// IncludedTaskfile represents information about included tasksfile
+type IncludedTaskfile struct {
+	Taskfile       string
+	Dir            string
+	AdvancedImport bool
+}
+
+// IncludedTaskfiles represents information about included tasksfiles
+type IncludedTaskfiles = map[string]IncludedTaskfile
+
+// UnmarshalYAML implements yaml.Unmarshaler interface
+func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var str string
+	if err := unmarshal(&str); err == nil {
+		it.Taskfile = str
+		return nil
+	}
+
+	var includedTaskfile struct {
+		Taskfile string
+		Dir      string
+	}
+	if err := unmarshal(&includedTaskfile); err == nil {
+		it.Dir = includedTaskfile.Dir
+		it.Taskfile = includedTaskfile.Taskfile
+		it.AdvancedImport = true
+		return nil
+	}
+
+	return ErrCantUnmarshalIncludedTaskfile
+}
diff --git a/internal/taskfile/merge.go b/internal/taskfile/merge.go
index b9f415ba..d350e4b9 100644
--- a/internal/taskfile/merge.go
+++ b/internal/taskfile/merge.go
@@ -22,7 +22,7 @@ func Merge(t1, t2 *Taskfile, namespaces ...string) error {
 	}
 
 	if t1.Includes == nil {
-		t1.Includes = make(map[string]string)
+		t1.Includes = make(IncludedTaskfiles)
 	}
 	for k, v := range t2.Includes {
 		t1.Includes[k] = v
diff --git a/internal/taskfile/read/taskfile.go b/internal/taskfile/read/taskfile.go
index a08360d3..7b1db509 100644
--- a/internal/taskfile/read/taskfile.go
+++ b/internal/taskfile/read/taskfile.go
@@ -28,8 +28,13 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 		return nil, err
 	}
 
-	for namespace, path := range t.Includes {
-		path = filepath.Join(dir, path)
+	for namespace, includedTask := range t.Includes {
+		if filepath.IsAbs(includedTask.Taskfile) {
+			path = includedTask.Taskfile
+		} else {
+			path = filepath.Join(dir, includedTask.Taskfile)
+		}
+
 		info, err := os.Stat(path)
 		if err != nil {
 			return nil, err
@@ -44,6 +49,15 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
 		if len(includedTaskfile.Includes) > 0 {
 			return nil, ErrIncludedTaskfilesCantHaveIncludes
 		}
+
+		if includedTask.AdvancedImport {
+			for _, task := range includedTaskfile.Tasks {
+				if !filepath.IsAbs(task.Dir) {
+					task.Dir = filepath.Join(includedTask.Dir, task.Dir)
+				}
+			}
+		}
+
 		if err = taskfile.Merge(t, includedTaskfile, namespace); err != nil {
 			return nil, err
 		}
diff --git a/internal/taskfile/taskfile.go b/internal/taskfile/taskfile.go
index 52b707c8..bcb7653c 100644
--- a/internal/taskfile/taskfile.go
+++ b/internal/taskfile/taskfile.go
@@ -6,7 +6,7 @@ type Taskfile struct {
 	Expansions int
 	Output     string
 	Method     string
-	Includes   map[string]string
+	Includes   IncludedTaskfiles
 	Vars       Vars
 	Env        Vars
 	Tasks      Tasks
@@ -20,7 +20,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
 		Expansions int
 		Output     string
 		Method     string
-		Includes   map[string]string
+		Includes   IncludedTaskfiles
 		Vars       Vars
 		Env        Vars
 		Tasks      Tasks
diff --git a/task.go b/task.go
index 8c1b14b7..5c4f07d6 100644
--- a/task.go
+++ b/task.go
@@ -207,6 +207,14 @@ func (e *Executor) Setup() error {
 		}
 	}
 
+	if v < 3 {
+		for _, taskfile := range e.Taskfile.Includes {
+			if taskfile.AdvancedImport {
+				return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
+			}
+		}
+	}
+
 	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 {


Test patch:
diff --git a/task_test.go b/task_test.go
index 5877088d..c752e95b 100644
--- a/task_test.go
+++ b/task_test.go
@@ -540,14 +540,32 @@ func TestIncludes(t *testing.T) {
 		Target:    "default",
 		TrimSpace: true,
 		Files: map[string]string{
-			"main.txt":               "main",
-			"included_directory.txt": "included_directory",
-			"included_taskfile.txt":  "included_taskfile",
+			"main.txt":                                  "main",
+			"included_directory.txt":                    "included_directory",
+			"included_directory_without_dir.txt":        "included_directory_without_dir",
+			"included_taskfile_without_dir.txt":         "included_taskfile_without_dir",
+			"./module2/included_directory_with_dir.txt": "included_directory_with_dir",
+			"./module2/included_taskfile_with_dir.txt":  "included_taskfile_with_dir",
 		},
 	}
 	tt.Run(t)
 }
 
+func TestIncorrectVersionIncludes(t *testing.T) {
+	const dir = "testdata/incorrect_includes"
+	expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
+
+	var buff bytes.Buffer
+	e := task.Executor{
+		Dir:    dir,
+		Stdout: &buff,
+		Stderr: &buff,
+		Silent: true,
+	}
+
+	assert.EqualError(t, e.Setup(), expectedError)
+}
+
 func TestIncludesEmptyMain(t *testing.T) {
 	tt := fileContentTest{
 		Dir:       "testdata/includes_empty",
diff --git a/testdata/includes/Taskfile.yml b/testdata/includes/Taskfile.yml
index 6b7f29ef..e94099e1 100644
--- a/testdata/includes/Taskfile.yml
+++ b/testdata/includes/Taskfile.yml
@@ -1,8 +1,18 @@
-version: '2'
+version: '3'
 
 includes:
   included: ./included
   included_taskfile: ./Taskfile2.yml
+  included_without_dir:
+    taskfile:  ./module1
+  included_taskfile_without_dir:
+    taskfile:  ./module1/Taskfile.yml
+  included_with_dir:
+    taskfile:  ./module2
+    dir:  ./module2
+  included_taskfile_with_dir:
+    taskfile:  ./module2/Taskfile.yml
+    dir:  ./module2
 
 tasks:
   default:
@@ -10,6 +20,10 @@ tasks:
       - task: gen
       - task: included:gen
       - task: included_taskfile:gen
+      - task: included_without_dir:gen_file
+      - task: included_taskfile_without_dir:gen_dir
+      - task: included_with_dir:gen_file
+      - task: included_taskfile_with_dir:gen_dir
 
   gen:
     cmds:
diff --git a/testdata/includes/Taskfile2.yml b/testdata/includes/Taskfile2.yml
index dbb4a34c..858fb38b 100644
--- a/testdata/includes/Taskfile2.yml
+++ b/testdata/includes/Taskfile2.yml
@@ -1,4 +1,4 @@
-version: '2'
+version: '3'
 
 tasks:
   gen:
diff --git a/testdata/includes/included/Taskfile.yml b/testdata/includes/included/Taskfile.yml
index e8fe2ad2..93b82347 100644
--- a/testdata/includes/included/Taskfile.yml
+++ b/testdata/includes/included/Taskfile.yml
@@ -1,4 +1,4 @@
-version: '2'
+version: '3'
 
 tasks:
   gen:
diff --git a/testdata/includes/module1/Taskfile.yml b/testdata/includes/module1/Taskfile.yml
new file mode 100644
index 00000000..3659073e
--- /dev/null
+++ b/testdata/includes/module1/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '3'
+
+tasks:
+  gen_dir:
+    cmds:
+      - echo included_directory_without_dir > included_directory_without_dir.txt
+
+  gen_file:
+    cmds:
+      - echo included_taskfile_without_dir > included_taskfile_without_dir.txt
diff --git a/testdata/includes/module2/Taskfile.yml b/testdata/includes/module2/Taskfile.yml
new file mode 100644
index 00000000..09bbdb60
--- /dev/null
+++ b/testdata/includes/module2/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '3'
+
+tasks:
+  gen_dir:
+    cmds:
+      - echo included_directory_with_dir > included_directory_with_dir.txt
+
+  gen_file:
+    cmds:
+      - echo included_taskfile_with_dir > included_taskfile_with_dir.txt
diff --git a/testdata/incorrect_includes/Taskfile.yml b/testdata/incorrect_includes/Taskfile.yml
new file mode 100644
index 00000000..63f4745b
--- /dev/null
+++ b/testdata/incorrect_includes/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '2.6'
+
+includes:
+  included:
+    taskfile:  ./included
+
+tasks:
+  default:
+    cmds:
+      - task: gen
diff --git a/testdata/incorrect_includes/included/Taskfile.yml b/testdata/incorrect_includes/included/Taskfile.yml
new file mode 100644
index 00000000..a50cc0ba
--- /dev/null
+++ b/testdata/incorrect_includes/included/Taskfile.yml
@@ -0,0 +1,6 @@
+version: '2.6'
+
+tasks:
+  gen:
+    cmds:
+      - echo incorrect includes test