feat: run default task in included file when task is omitted
I was experimenting with refactoring my (now very large) Taskfile into smaller, more manageable files and I came across #661. This functionality would really help us to make our tasks a bit simpler.
This PR adds the functionality mentioned by @tylermmorton and @andreynering in that issue. Namely:
- When a Taskfile with a
defaulttask inside is included, thedefaulttask is available by callingtask <namespace>as opposed totask <namespace>:default. - If the parent Taskfile contains a task with the same name as the namespace (i.e. the task is "shadowed"), the task in the parent is run, and not the default task in the included file.
- This ensures that we maintain backward compatibility with existing Taskfiles.
- A couple of unit tests for ensuring the functionality works going forwards.
Note: Not sure about the "shadowed" terminology. Open to better suggestions.
An example below:
# Taskfile.yml
version: '3'
includes:
included:
taskfile: Taskfile2.yml
# Taskfile2.yml
version: '3'
tasks:
default:
cmds:
- echo "included task"
task included will output: included task.
Currently you are required to run task included:default.
If we amend the parent taskfile like so:
# Taskfile.yml
version: '3'
includes:
included:
taskfile: Taskfile2.yml
tasks:
included:
cmds:
- echo "shadowed task"
task included will now output: shadowed task instead.
I've just seen #665. I didn't realise someone had already made an attempt at this. However, since the comments there don't seem to have been addressed, I've made an attempt to address them here instead.
I think this implementation has unintended side-effects. Tasks assigned here will be duplicated on task --list.
@andreynering I tried out your suggestion in variables.go, but couldn't get it to work. This duplication (as I'm sure you know) is happening because we're merging the included task and then copying it to a new map key as well. The names are the same due to them sharing a memory address and the loop at the end of the read.Taskfile function is setting the name for both map keys. Interestingly, because looping over maps is non-deterministic, the name is sometimes docs:default and sometimes docs depending on which map entry is looped over last.
I can see two fixes to this depending on the desired behaviour:
- Copy the value so that they no longer share a pointer. This results in both
docsanddocs:defaultbeing written to the--listoutput.
if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
t.Tasks[namespace] = &taskfile.Task{}
*t.Tasks[namespace] = *(includedTaskfile.Tasks["default"])
}
- Delete the original map entry. This will result in only
docsbeing written to the--listoutput.
if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
t.Tasks[namespace] = includedTaskfile.Tasks["default"]
delete(t.Tasks, namespace+":default")
}
IMO, the first is preferrable, since both docs and docs:default are still callable (and should remain so for backward compatibility). The second approach also breaks many tests 😞
Awesome. Thanks @pd93!