Compare commits

...

186 Commits

Author SHA1 Message Date
Pete Davison
1c35358fcc v3.43.3 2025-04-27 22:29:34 +00:00
Pete Davison
13daa6dc35 feat: formatting with golangci-lint and gci 2025-04-27 22:28:42 +00:00
Pete Davison
20c1ffe098 docs: update variables example so that it doesn't error 2025-04-27 22:26:59 +00:00
Pete Davison
bd8ccb8d03 chore: changelogs for reverts 2025-04-27 22:26:29 +00:00
Pete Davison
8162b05f59 Revert "feat: process variables in include vars (#2113)"
This reverts commit f0414f162d.
2025-04-27 22:15:49 +00:00
Pete Davison
68d5095761 Revert "fix: .USER_WORKING_DIR should contain the value of --dir if given (#2186)"
This reverts commit 768dca053b.
2025-04-27 22:14:50 +00:00
Andrey Nering
6cb0a5a2f2 v3.43.2 2025-04-21 16:35:01 -03:00
Andrey Nering
08056924e0 chore: add changelog entry for #2191 2025-04-21 16:33:30 -03:00
Valentin Maerten
39706105e1 fix: CLI_ARGS is a string and not an array (#2191) 2025-04-21 16:31:18 -03:00
Andrey Nering
bf4e7960cb chore: show right version on changelog 2025-04-21 14:31:25 -03:00
Andrey Nering
3d36616e9e v3.43.1 2025-04-21 13:57:43 -03:00
Andrey Nering
3976e8372a chore: move the experiments package out of the internal/ dir
Closes #2014
2025-04-21 13:55:56 -03:00
Andrey Nering
c2123dc016 v3.43.0 2025-04-21 13:50:40 -03:00
Andrey Nering
0a6cd1ee42 chore: add changelog entry for #2173 2025-04-21 13:48:25 -03:00
Valentin Maerten
7169bf6434 fix: interpolate vars in defer (#2173) 2025-04-21 13:43:20 -03:00
renovate[bot]
84cd4dfdad chore(deps): update all non-major dependencies (#2188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 13:52:23 +02:00
Pete Davison
672b39413f feat: mockery v3 (#2110) 2025-04-19 12:55:22 +01:00
Pete Davison
7eebf6e704 chore: delete unused exp package 2025-04-19 11:54:48 +00:00
Pete Davison
4834ac743c chore: changelog for #2166 2025-04-19 11:54:27 +00:00
Pete Davison
c5afffb551 feat: recursive config search (#2166)
* refactor: experiments flags

* refactor: args.Parse

* feat: recursive search for taskrc files

* feat: consolidate some code into new fsext package

* feat: add tests for search and default dir

* fix: linting issues
2025-04-19 12:20:33 +01:00
Pete Davison
1ae3bf0b25 chore: changelog for #2176 2025-04-19 11:20:17 +00:00
Pete Davison
a84f09d45f feat: remote taskfile improvements (cache/expiry) (#2176)
* feat: cache as node, RemoteNode and cache-first approach

* feat: cache expiry

* feat: pass ctx into reader methods instead of timeout

* docs: updated remote taskfiles experiment doc

* feat: use cache if download fails
2025-04-19 12:12:08 +01:00
Pete Davison
f47f237093 chore: changelog for #2169 2025-04-19 11:11:51 +00:00
Pete Davison
04df108fb5 docs: package api docs (#2169)
* refactor: pass Node into Read method instead of Reader type

* docs: add "key packages" and "Reading Taskfiles" sections to package doc
2025-04-19 11:58:31 +01:00
Pete Davison
8885d9e4f7 chore: changelog for #2075 2025-04-19 10:57:36 +00:00
Pete Davison
a60c2ec3f8 fix: sources brace expansion (#2075) 2025-04-19 11:51:31 +01:00
Andrey Nering
f789c57624 chore: add changelog entry for #2134 2025-04-18 22:56:46 -03:00
atusy
7416b7d77e feat(completion): let fish complete global tasks if --global (-g) is passed (#2134) 2025-04-18 22:55:53 -03:00
Andrey Nering
c1ab661cf2 chore: add changelog entry for #2102, #2103 and #2186 2025-04-18 22:32:19 -03:00
Andrey Nering
768dca053b fix: .USER_WORKING_DIR should contain the value of --dir if given (#2186)
Closes #2102
Closes #2103

Co-authored-by: jaynis <kranz.jannis@googlemail.com>
2025-04-18 22:27:30 -03:00
Jay Berkenbilt
e65159f613 docs: clarify --dir flag (#2123)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2025-04-17 23:10:57 +00:00
Pete Davison
789a7ea950 docs: things aren't always simple 2025-04-16 15:59:35 +00:00
renovate[bot]
b11da93c78 chore(deps): update dependency @types/react to v19.1.1 (#2178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 10:38:04 -03:00
renovate[bot]
8c720b03aa chore(deps): update all non-major dependencies (#2167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 10:29:35 -03:00
Pete Davison
8c8b1b5f3b fix: make version semver compliant 2025-04-11 09:04:03 +00:00
Pete Davison
38b42d0fb1 fix: experiments validation should happen before command flags are evaluated 2025-04-10 08:45:55 +00:00
Pete Davison
669bf33619 chore: changelog for #2151 2025-04-05 23:15:02 +00:00
Artem Sedykh
6f0f38b8d9 feat: support for loops with generates (#2151) 2025-04-05 23:55:43 +01:00
Pete Davison
a9de239e38 chore: changelog for #2113 2025-04-05 22:16:30 +00:00
Pete Davison
f0414f162d feat: process variables in include vars (#2113)
* feat: process variables in include vars

* feat: add test for include variables
2025-04-05 23:12:54 +01:00
Pete Davison
a24f4958cd chore: changelog for #2131 2025-04-05 22:12:18 +00:00
Pete Davison
55790be6ad feat: better versioning (#2131) 2025-04-05 23:09:27 +01:00
Andrey Nering
88fdbd13cf ci: use goreleaser pro
Thanks @caarlos0 for the free key!
2025-04-05 18:20:32 -03:00
Pete Davison
566ac29932 feat: update golangci-lint version in ci 2025-04-05 17:58:50 -03:00
Pete Davison
ffef3ed1a6 feat: migrate to golangci-lint v2 2025-04-05 17:58:50 -03:00
Andrey Nering
2a60842707 chore: remove some repo files that were moved to the .github repo
https://github.com/go-task/.github
2025-04-05 17:51:56 -03:00
renovate[bot]
41bd866813 chore(deps): update all non-major dependencies (#2165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 21:39:07 +02:00
Iain Majer
01bc0a0529 docs: specify that install command parameters are order specific (#2115)
Co-authored-by: Iain Majer <iain.majer@river-island.com>
2025-04-05 21:28:36 +02:00
Valentin Maerten
a6a9792b7e docs: use .taskrc instead of .task-experiments.yml in our docs (#2157)
* docs: use .taskrc instead of .task-experiments.yml in our docs

* fix formatting

* fix: whitespace in experiments.mdx

---------

Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2025-04-03 13:29:00 +02:00
dependabot[bot]
ce032dc46b chore(deps): bump image-size from 1.2.0 to 1.2.1 in /website (#2152) 2025-04-02 12:55:15 -03:00
Pete Davison
f07f4c85b2 chore: changelog for #2148 2025-04-01 13:56:41 +00:00
Pete Davison
cd81d94e18 feat: better functional options for reader (#2148) 2025-04-01 14:51:25 +01:00
Pete Davison
1939f83ffe chore: changelog for #2147 2025-03-31 20:50:14 +00:00
Pete Davison
2a92b70bc2 feat: better functional options (#2147) 2025-03-31 21:49:00 +01:00
Pete Davison
4736bc2734 refactor: unify how executor tests are written (#2042)
* feat: use TaskTest for executor tests

* feat: more tests

* feat: separate tests for executing and formatting with new functional options that work for both test types

* feat: formatter tests

* refactor: more tests
2025-03-31 17:53:58 +01:00
Andrey Nering
180fcef364 docs: fix typo: source -> sources 2025-03-31 10:50:33 -03:00
renovate[bot]
f6baa5942e chore(deps): update all non-major dependencies to v19.0.12 (#2137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 09:32:13 -03:00
Pete Davison
d54b0d6a2a chore: changelog for #2144 2025-03-30 19:22:37 +00:00
Pete Davison
03b242d4c3 fix: bug where undefined/null variables resolve to "" instead of nil (#2144) 2025-03-30 19:21:02 +00:00
Pete Davison
60e28ecdcc chore: changelog for #2121 2025-03-26 22:21:47 +00:00
Pete Davison
dd8daa68cd feat: allow wildcards to match multiple tasks (#2121)
* feat: allow wildcards to match multiple tasks

* docs: improved wildcard section
2025-03-26 22:17:27 +00:00
Pete Davison
55617e062f chore: changelog for #2081 2025-03-26 21:50:53 +00:00
Pete Davison
c6f1b3ae4f feat: make map variables experiment (prop 2) generally available (#2081)
* feat: make map variables experiment (prop 2) generally available

* docs: remove map variables experiment page and update usage to include map variable info
2025-03-26 21:40:09 +00:00
Andrey Nering
cb14a4f3a1 chore: add changelog for #2048 2025-03-22 20:15:26 -03:00
Andrey Nering
0d5f2b5dab feat(watcher): migrate to fsnotify (#2048) 2025-03-22 20:06:16 -03:00
Andrey Nering
89caf1e049 chore: add changelog for #2130 2025-03-19 10:36:07 -03:00
atusy
7f7e8306da fix(fish): fish completion error due to variable shadowing (#2130) 2025-03-19 13:33:32 +00:00
renovate[bot]
1f2eecda9e chore(deps): update dependency go to 1.24.x (#2126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 08:57:58 -03:00
renovate[bot]
60c959c75c chore(deps): update tj-actions/changed-files action to v46 (#2127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 08:57:33 -03:00
Pete Davison
a771e91ff3 chore: changelog for #2125 2025-03-16 13:20:36 +00:00
Timothy Rule
532644d7f8 feat: create NoSort sorter for CLI sort option "none" (#2125) 2025-03-16 13:17:14 +00:00
Pete Davison
b68f4067d9 chore: changelog for #2112 2025-03-12 19:59:54 +00:00
Pete Davison
c544b0058d fix: labels for experiments 2025-03-12 19:57:33 +00:00
Pete Davison
d1360ee72a refactor: embed the default Taskfile instead of defining it in code (#2112) 2025-03-11 13:53:08 +00:00
Pete Davison
076aff1f8e chore: changelog for #2085 2025-03-10 20:41:47 +00:00
Pete Davison
ffeb3bcc3f refactor: executor functional options (#2085)
* refactor: executor functional options

* refactor: minor tidy up of list code

* fix: WithVersionCheck missing from call to NewExecutor

* feat: docstrings for structs with functional options

* refactor: prefix the functional options with the name of the struct they belong to
2025-03-10 20:38:25 +00:00
Pete Davison
8181352d54 v3.42.1 2025-03-10 20:18:58 +00:00
Pete Davison
23fd7e782c chore: changelog for #2107 2025-03-10 11:48:10 +00:00
Pete Davison
6604b9a8cc fix: special variable type errors in vars with no task context (#2107)
* fix: stop dotenv trying to fetch variables when no dotenv specified

* fix: set special variables to "" when they can't be calculated
2025-03-10 11:46:07 +00:00
Andrey Nering
6ee1053c96 docs: fix link to nix package file 2025-03-08 22:53:58 -03:00
Andrey Nering
8eaf83599e fix(goreleaser): fix a deprecation warning 2025-03-08 22:44:05 -03:00
Andrey Nering
cd086228b2 v3.42.0 2025-03-08 22:34:07 -03:00
Andrey Nering
1b8b399c7e fix(changelog): add missing # to issue number 2025-03-08 22:32:55 -03:00
renovate[bot]
8426f84b18 chore(deps): update all non-major dependencies (#2097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 08:24:04 +01:00
sblondon
14bbb324e5 doc: fix: remove a word (#2093)
The 'you' word has no sense here
2025-02-27 17:29:52 +00:00
Valentin Maerten
b9d202c491 chore: changelog for #2092 2025-02-26 18:07:31 +01:00
Valentin Maerten
c23c46e326 fix: include with dynamic vars (#2092) 2025-02-26 17:49:05 +01:00
Oleksandr Redko
a266fba93e chore: add linter mirror (#2060) 2025-02-24 09:06:54 -03:00
Pete Davison
fb631902ce refactor: run task through modernize tool (#2088) 2025-02-24 11:59:50 +00:00
Pete Davison
b14125bacd fix: remove debug line 2025-02-24 02:16:53 +00:00
Pete Davison
3c5782f4a4 chore: changelog for #2084 2025-02-23 18:31:18 +00:00
Pete Davison
60c8ee0ce6 refactor: ast.Call should be in main task package (#2084) 2025-02-23 18:30:42 +00:00
Pete Davison
cdaf69e03d chore: changelog for #2069 2025-02-23 18:18:22 +00:00
Pete Davison
d6234af49a feat: allow variable references in a matrix (#2069) 2025-02-23 18:13:56 +00:00
renovate[bot]
a31f2cf4a8 chore(deps): update all non-major dependencies (#2064) 2025-02-23 15:10:00 -03:00
Pete Davison
0dd6f78855 chore: changelog for #2086 2025-02-23 18:00:00 +00:00
Pete Davison
6f80777faf docs: getting started (#2086)
* docs: getting started

* docs: update intro with links to getting started docs
2025-02-23 17:56:55 +00:00
Valentin Maerten
8558e0c48a chore: changelog for #1982 2025-02-23 10:54:45 +01:00
Valentin Maerten
461714a899 feat: add a new .taskrc.yml to enable experiments (#1982) 2025-02-23 10:51:59 +01:00
Pete Davison
8a35033abc chore: changelog for #1798 2025-02-22 16:27:34 +00:00
Pete Davison
daf39a04bf feat: iterators (#1798)
* feat: update to github.com/elliotchance/orderedmap/v3

* refactor: better sort package

* feat: iterators

* chore: remove unnecessary code
2025-02-22 16:22:03 +00:00
Pete Davison
25f9299d0a chore: changelog for #2082 2025-02-22 16:09:53 +00:00
Pete Davison
4d15a8be8f feat: remove logger from taskfile package (#2082)
* refactor: remove logger from the taskfile node interface

* refactor: functional options on taskfile.Reader

* feat: use pass in debug/prompt functions to Reader rather than task Logger

* chore: reader docstrings

* fix: typo
2025-02-22 16:00:37 +00:00
Pete Davison
cbde4c33f8 chore: changelog for #2068 and #2052 2025-02-22 15:58:47 +00:00
Pete Davison
cdb6a3f70a feat: decoding improvements (#2068)
* refactor: moved/simplified snippets into its own file with tests

* refactor: move snippet to taskfile package

* feat: support snippets with line/col = 0

* feat: functional options for snippets

* feat: added option to hide snippet indicators

* feat: store raw lines for length calculations

* feat: add debug function for TaskfileDecodeError

* fix: decode errors from commands

* fix: schema for defer cmd calls

* fix: linting issues

* refactor: split var and vars into different files like other structures
2025-02-22 15:44:22 +00:00
Valentin Maerten
fb27318601 chore: changelog for #2052 2025-02-20 20:08:22 +01:00
Valentin Maerten
35ea4e0460 feat: display allowed values when vars are not provided (#2052) 2025-02-20 20:08:14 +01:00
Pete Davison
2b4d9bfba7 chore: changelog for #2059 2025-02-11 22:44:33 +00:00
Pete Davison
ce96447468 chore: bump minimum version to 1.23 (#2059)
* chore: bump minimum version to 1.23

* fix: version package for 1.24

* feat: update golangci-lint version
2025-02-11 22:43:17 +00:00
Pete Davison
e7a6de64cb chore: add package API changes to changelog and add gorelease tool (#2055)
* chore: add package API changes to changelog and add gorelease tool

* chore: use bullet points instead of a paragraph
2025-02-10 16:16:44 +00:00
Pete Davison
ff8c913ce7 chore: changelog and minor adjustments for #2018 2025-02-10 11:24:32 +00:00
Henrique Corrêa
0e23404d23 feat: specify --init filename/path (#2018)
* feat: specify init filename with --taskfile flag

previously, it was not possible to specify which filename to use when initializing a new Taskfile as it was hardcoded as "Taskfile.yml".

now the --taskfile flag specifies where to write the file to, and the first * contained in it will be replaced by "Taskfile", so `task -it *.yaml` will create a `Taskfile.yaml` file.

* docs: update CLI reference

* fix Flags header being inside tip admonition
* change -t flag's default column and add a description
* add Default Filenames section

* docs: revert adding Default Filenames section

I didn't realize it already existed elsewhere.

* refactor: use path instead of filepath on InitTaskFile

as requested to prevent ambiguity with the stdlib package.

* fix TestInit (incorrectly merged)

* docs: remove outdated info on --taskfile flag

* refactor task initialization changes

- remove const DefaultTaskInitFilename from taskfile/taskfile.go
- revert description of Entrypoint flag
- make InitTaskfile accept a path to either a file or a directory, and join the default Taskfile name+ext to it if it is a directory
- take the target file path from the first argument instead of the Entrypoint flag
- detect extension-only filenames (".yaml") instead of replacing "*" with "Taskfile"
- use different format in success log so that it makes sense at different paths than the current dir

* print colon instead of "at"

it's a lot cleaner in most cases.

* rewrite init tests

test both initializing to a directory path and a file path

* return final path from InitTaskfile

...and print it's relative representation

* fix lint error (ineffassign)

* use filepathext.TryAbsToRel() instead

* define and use filepathext.IsExtOnly()

* link to default filenames list in cli ref docs

(specifically in the --taskfile flag description)
2025-02-10 11:22:49 +00:00
renovate[bot]
65a64a01ee chore(deps): update all non-major dependencies (#2054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 07:53:45 -03:00
Pete Davison
f6ec7444d5 chore: changelog for #2049 2025-02-08 23:16:41 +00:00
Pete Davison
6ce798e16c feat: experiments logging improvements (#2049)
* feat: warn when enabling inactive experiments

* feat: TASK_ environment prefix

* feat: calculate experiment enabled/active instead of storing

* refactor: rename GetTaskVar to GetTaskEnv

* feat: experiments tests
2025-02-08 23:02:51 +00:00
Pete Davison
be81885835 feat: stop task test installing task (#2050) 2025-02-08 23:02:22 +00:00
Valentin Maerten
69ac06170a chore: changelog for #2031 2025-02-08 17:34:43 +01:00
Valentin Maerten
c995fe6d11 fix(checker): use only one checker at the same time to improve perf (#2031)
* fix(checker): use only one checker at the same time to improve performance

* refactor

* fix test
2025-02-08 17:34:04 +01:00
Valentin Maerten
9009124192 chore: changelog for #2033 2025-02-08 17:31:01 +01:00
Valentin Maerten
80f96d67da fix: requires allowed values works with dynamic var (#2033) 2025-02-08 17:29:36 +01:00
Valentin Maerten
002b8c929a docs: fix a typo in dotenv section 2025-02-08 16:13:34 +01:00
Pete Davison
b5b1524d3a feat: variable inheritance tests (#2038) 2025-02-05 19:51:52 +00:00
renovate[bot]
3aee0a0519 chore(deps): update react monorepo to v19 (#2028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 10:32:44 +01:00
Pete Davison
23df1f0c61 chore: changelog for #2007 2025-01-29 22:49:14 +00:00
Ukjae Jeong
edbb83f6de fix: HTTPNode.Location when building graph (#2007)
* Fix HTTPNode.Location when building graph

* Add test and fix cache
2025-01-29 22:46:43 +00:00
Pete Davison
c903d5c6f4 chore: changelog for #2011 2025-01-29 22:43:51 +00:00
Henrique Corrêa
88c4ba1740 feat: make Taskfile initialization less verbose by default (#2011)
* change what is printed when creating Taskfile

When using --init to create a new Taskfile, it used to print the whole contents of the file to the terminal, which was unnecessarily verbose (and honestly felt unintentional).

Now only the filename is printed by default and the --silent and --verbose flags can be used to control the behavior (print nothing or content + filename, respectively).

* include additional new line with -i -v

it looks slightly better in the terminal.

* print init success text in green

* fix TestInit, create and pass in a logger

* move logging outside of InitTaskfile

- revert API changes made to InitTaskfile
- make consts in init.go public so they can be accessed from task.go
- rename variable "logger" to "log" in task.go to fix conflict with logger package

* move TestInit into init_test.go file

as requested by pd93.
2025-01-29 22:41:17 +00:00
Valentin Maerten
7d4c52546a chore: add label to renovate's PRs 2025-01-29 21:46:37 +01:00
Pete Davison
f5121de468 docs: broken links 2025-01-26 00:56:25 +00:00
renovate[bot]
b5d573fbd9 chore(deps): update golang's deps (#2020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 09:22:10 +01:00
renovate[bot]
888de0f8ef chore(deps): update website (#2021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 09:14:26 +01:00
Valentin Maerten
09b11d343b chore: remove dependabot and put Renovate weekly (#2017) 2025-01-25 09:06:53 +01:00
Andrey Nering
a2390d0dca v3.41.0 2025-01-18 11:15:57 -03:00
Andrey Nering
0f633091eb chore: fix typo on changelog 2025-01-18 10:54:07 -03:00
Andrey Nering
6b16c532c2 chore: add changelog for #1938 2025-01-18 10:27:21 -03:00
Lea Anthony
69f5714e45 fix: disable version check for use as an external library
Closes #1938
2025-01-18 10:26:58 -03:00
Andrey Nering
b3e4cfcf48 refactor: use modern loop syntax
ref #1980
2025-01-18 10:11:00 -03:00
EinoPlasma
65a71e5df3 refactor: signal handling to improve clarity and correctness (#1980) 2025-01-18 13:09:36 +00:00
jonathanagustin
bad2c8fcc1 docs: fix some examples to use spaces instead of tabs (#2002) 2025-01-18 10:04:39 -03:00
dependabot[bot]
97f41b710e chore(deps): bump github.com/go-git/go-git/v5 from 5.13.0 to 5.13.1 (#1992)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-05 16:51:59 +01:00
Pete Davison
240047152d chore: changelog for #1869 2025-01-02 20:11:00 +00:00
rohm1
24a830e384 fix: forward env to RunCommand when evaluating sh vars (#1869)
* forward env to RunCommand when evaluating sh vars. fixes #1742

* feat: added tests

* fix: test

---------

Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2025-01-02 20:07:25 +00:00
Pete Davison
fe9f489702 chore: changelog for #1989 2025-01-02 18:23:06 +00:00
Pete Davison
27de441ed2 docs: updated installation guides (#1989)
* docs: updated installation guides

* fix: installation method titles in sidebar

* feat: add instructions for pip

* fix: anchors in installation doc
2025-01-02 18:20:29 +00:00
renovate[bot]
79f7af2b04 fix(deps): update golang (#1983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 14:46:28 -03:00
Oren
b588d49cfb fix typo in usage.mdx (#1985) 2025-01-02 17:43:40 +00:00
renovate[bot]
45006e2ce0 fix(deps): update website (#1984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 11:30:59 +01:00
Pete Davison
e5d8237053 docs: package api doc (#1981) 2024-12-31 16:08:58 +00:00
Valentin Maerten
89740ed72a chore: changelog for #1872 2024-12-31 16:06:44 +01:00
Niklas Rousset
c1e14c461b feat: make CHECKSUM and TIMESTAMP vars available in cmds commands (#1872) 2024-12-31 16:04:42 +01:00
Pete Davison
dc2eceb634 chore: update experiments field in issue template 2024-12-30 22:09:45 +00:00
Andrey Nering
43f3dcea05 chore(requires): skip unneeded variable evaluation for requires (#1976) 2024-12-30 18:27:16 -03:00
Valentin Maerten
f27daea5c9 chore: changelog for #1960 2024-12-30 19:17:55 +01:00
Pete Davison
49e88e92cf chore: changelogs for #1797, #1972 and #1974 2024-12-30 18:13:43 +00:00
Valentin Maerten
da40aabcc7 fix: task level vars are not ignored in requires (#1960)
Co-authored-by: MOKEKO <Anthurium.1605@gmail.com>
2024-12-30 19:11:27 +01:00
Pete Davison
8ce9bdc8c7 refactor: remove pointer from prefixed mutex 2024-12-30 18:03:29 +00:00
Graham Dennis
0409c3c3ba fix: concurrent mutations to prefixWriter (#1974) 2024-12-30 18:02:34 +00:00
Pete Davison
fd3532812e fix: orderedmap race condition (#1972) 2024-12-30 17:58:45 +00:00
Pete Davison
2965841eb7 feat: use external package for ordered maps (#1797) 2024-12-30 17:54:36 +00:00
Andrey Nering
dbe6e41ac8 chore(website): update navbar icons 2024-12-30 14:36:44 -03:00
Valentin Maerten
8f73ced037 fix: missing t.Parallel in some tests 2024-12-30 11:51:13 +01:00
Valentin Maerten
2a4f93eb41 chore: changelog for #1961 2024-12-30 11:51:13 +01:00
Valentin Maerten
9d8c4ba7e6 feat: add TASK_DIR special variable (#1961)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2024-12-30 11:45:25 +01:00
Valentin Maerten
1bda388925 chore: changelog for #1962 2024-12-30 10:20:07 +01:00
Valentin Maerten
d64df3f9d7 fix: evaluate requires before compiled task (#1962) 2024-12-30 10:15:17 +01:00
Valentin Maerten
d1f18d36b8 chore: changelog for #1859 2024-12-30 10:10:11 +01:00
Valentin Maerten
5f1d46c770 feat: can exclude task from being included (#1859) 2024-12-30 10:09:28 +01:00
Jonathan Rayner
9727eef476 docs: update Arch installation instructions (#1971) 2024-12-29 15:13:14 +01:00
Andrey Nering
c5be676555 chore(website): mention devowl.io as a gold sponsor 2024-12-20 10:23:01 -03:00
Pete Davison
f3317266dc feat: issue templates (#1963) 2024-12-16 12:36:45 -06:00
Andrey Nering
36ff00e3f9 chore: change some mentions of project authors 2024-12-11 22:04:41 -03:00
Lumberjackz
041063b732 docs: fix typo (#1895) 2024-12-11 21:50:43 -03:00
CeMoN24
2ab1dcbf1d docs: fix typo (#1896) 2024-12-11 21:50:05 -03:00
Matthias Vogt
24a0f24835 docs: fix typo (#1897) 2024-12-11 21:49:42 -03:00
christiandins
4dffab2e0a feat: add parallel test execution to improve runtime (#1882) 2024-12-11 21:47:10 -03:00
christiandins
b9a5d1c573 lint: add linter noctx (#1898) 2024-12-11 21:42:04 -03:00
renovate[bot]
e1818e9e31 chore(deps): update node.js to v22 (#1899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 21:36:05 -03:00
dependabot[bot]
bb2de3fdf9 chore(deps): bump golang.org/x/crypto from 0.25.0 to 0.31.0 (#1949)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 00:24:44 +00:00
Martijn Pieters
82f6029043 docs: document defer task options (#1907) 2024-12-11 21:23:10 -03:00
Valentin Maerten
cfaecf8b4c chore: changelog for #1879 2024-12-07 16:16:47 +01:00
Danilo Bürger
4595c1e32a feat: add silent for defer (#1879)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2024-12-07 16:16:27 +01:00
dependabot[bot]
1a648dea50 chore(deps): bump golang.org/x/term from 0.26.0 to 0.27.0 (#1942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-07 16:11:53 +01:00
renovate[bot]
a273183745 chore(deps): update website (#1935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 16:11:20 +01:00
dependabot[bot]
e2243fc6d9 chore(deps): bump golang.org/x/sync from 0.9.0 to 0.10.0 (#1941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-07 16:08:37 +01:00
Valentin Maerten
c1209d9f13 chore: changelog for #1921 2024-12-07 16:06:16 +01:00
mgbowman
2b54b04cfc fix: dynamic variable output in verbose mode (#1921)
Co-authored-by: Matthew Bowman <mbowman@nvidia.com>
2024-12-07 16:05:53 +01:00
411 changed files with 14391 additions and 9352 deletions

View File

@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true
indent_style = tab
[*.{md,mdx,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
[*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,css,svg,sh,bash,fish}]
indent_style = space
indent_size = 2

View File

@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey@nering.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,14 +0,0 @@
## You can find our [contribution guide on our website][contributing]
- Please read it carefully before opening a PR.
- If you have any questions, you can:
- [Open an issue][issues]
- [Create a discussion][discussions]
- [Chat to us on Discord][discord]
<!-- prettier-ignore-start -->
[contributing]: https://taskfile.dev/contributing
[issues]: https://github.com/go-task/task/issues
[discussions]: https://github.com/go-task/task/discussions
[discord]: https://discord.gg/6TY36E39UK
<!-- prettier-ignore-end -->

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: [andreynering, pd93, vmaerten]
open_collective: task
custom: https://taskfile.dev/donate/

View File

@@ -1,20 +0,0 @@
---
name: Bug Report
about: Use this to report bugs and issues
---
<!--
Thanks for your bug report!
Before submitting this issue, please make sure the same problem was not
already reported by someone else.
Please describe the bug you're facing. Consider pasting example Taskfiles
showing how to reproduce the problem.
-->
- Task version:
- Operating system:
- Experiments enabled:

69
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: '🐞 Bug Report'
description: Report a bug in Task.
labels: ['state: needs-triage']
body:
- type: markdown
attributes:
value: |
Thanks for your bug report!
Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same bug was not already reported by someone else.
- type: textarea
id: description
attributes:
label: Description
description: Describe the bug you're seeing.
placeholder: |
- What did you do?
- What did you expect to happen?
- What happened instead?
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version(s) of Task is the issue occurring on?
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: What operating system(s) is the issue occurring on?
validations:
required: true
- type: dropdown
id: experiments
attributes:
label: Experiments Enabled
description: Do you have any experiments enabled? You can check by running `task --experiments`.
multiple: true
options:
- Env Precedence
- Gentle Force
- Map Variables (1)
- Map Variables (2)
- Remote Taskfiles
validations:
required: false
- type: textarea
id: logs
attributes:
label: Example Taskfile
description: |
If you have a Taskfile that reproduces the issue, please paste it here.
This will be automatically formatted into code, so no need for backticks.
render: YAML
placeholder: |
version: '3'
tasks:
default:
cmds:
- 'echo "This Taskfile is buggy :("'

View File

@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Extension for Visual Studio Code
- name: '🔌 Task for Visual Studio Code'
url: https://github.com/go-task/vscode-task
about: Issues related to the Visual Studio Code extension should be opened here.
- name: Help forum on Discord
url: https://discord.gg/6TY36E39UK
about: 'The Discord #help channel is the best way to get help from the community.'
- name: Questions, Ideas and General Discussions
about: 'Issues related to the Visual Studio Code extension should be opened here.'
- name: '💬 Help forum on Discord'
url: https://discord.com/channels/974121106208354339/1025054680289660989
about: 'The #help channel on our Discord is the best way to get help from the community.'
- name: '❓ Questions, Ideas and General Discussions'
url: https://github.com/go-task/task/discussions
about: Ask questions and discuss general ideas with the community.
about: 'Ask questions and discuss general ideas with the community.'

View File

@@ -1,15 +0,0 @@
---
name: Feature Request
about: Use this to make feature requests
---
<!--
Describe in detail what feature do you want to see in Task.
Give examples if possible.
Please, search if this wasn't proposed before, and if this is more like an idea
than a strong feature request, consider opening a
[discussion](https://github.com/go-task/task/discussions) instead.
-->

View File

@@ -0,0 +1,23 @@
name: '✨ Feature Request'
description: Suggest a new feature or enhancement for Task.
labels: ['state: needs-triage']
body:
- type: markdown
attributes:
value: |
Thanks for your feature request!
Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same change was not already requested by someone else.
If your request is more of an idea than a feature request, consider opening a [discussion](https://github.com/go-task/task/discussions) instead.
- type: textarea
id: description
attributes:
label: Description
description: Describe the feature/enhancement you want to see in Task.
placeholder: |
- Give a general overview of the feature/enhancement.
- Explain problem is the change trying to solve.
- Give examples of how you would use the feature.
validations:
required: true

View File

@@ -1,24 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
day: saturday
time: '08:00'
timezone: America/Sao_Paulo
labels:
- "area: dependencies"
- "lang: go"
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: saturday
time: '08:00'
timezone: America/Sao_Paulo
labels:
- "area: dependencies"
- "lang: javascript"

39
.github/renovate.json vendored
View File

@@ -3,46 +3,23 @@
"extends": [
"config:recommended",
"group:allNonMajor",
"schedule:monthly"
"schedule:weekly",
":semanticCommitTypeAll(chore)"
],
"mode": "full",
"reviewers": ["team:developer"],
"addLabels":["area: dependencies"],
"packageRules": [
{
"matchManagers": ["github-actions"],
"groupName": "Github Action",
"labels": ["area: github actions", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
"addLabels": ["area: github actions"]
},
{
"matchManagers": ["npm", "nvm"],
"groupName": "Website",
"labels": ["lang: javascript", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
"matchCategories": ["js", "node"],
"addLabels": ["lang: javascript"]
},
{
"matchManagers": ["gomod"],
"groupName": "golang",
"labels": ["lang: go", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
"matchCategories": ["golang"],
"addLabels": ["lang: go"]
}
]
}

View File

@@ -6,7 +6,7 @@ on:
jobs:
issue-experiment-proposed:
if: github.event.label.name == format('experiment{0} proposed', ':')
if: github.event.label.name == format('status{0} proposed', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -20,7 +20,7 @@ jobs:
body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-draft:
if: github.event.label.name == format('experiment{0} draft', ':')
if: github.event.label.name == format('status{0} draft', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -34,7 +34,7 @@ jobs:
body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-candidate:
if: github.event.label.name == format('experiment{0} candidate', ':')
if: github.event.label.name == format('status{0} candidate', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -48,7 +48,7 @@ jobs:
body: 'This experiment has been marked as a candidate! :fire: This means that the implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-stable:
if: github.event.label.name == format('experiment{0} stable', ':')
if: github.event.label.name == format('status{0} stable', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -62,7 +62,7 @@ jobs:
body: 'This experiment has been marked as stable! :metal: This means that the implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-released:
if: github.event.label.name == format('experiment{0} released', ':')
if: github.event.label.name == format('status{0} released', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -82,7 +82,7 @@ jobs:
state: 'closed'
})
issue-experiment-abandoned:
if: github.event.label.name == format('experiment{0} abandoned', ':')
if: github.event.label.name == format('status{0} abandoned', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
@@ -102,7 +102,7 @@ jobs:
state: 'closed'
})
issue-experiment-superseded:
if: github.event.label.name == format('experiment{0} superseded', ':')
if: github.event.label.name == format('status{0} superseded', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7

View File

@@ -13,7 +13,7 @@ jobs:
name: Lint
strategy:
matrix:
go-version: [1.22.x, 1.23.x]
go-version: [1.23.x, 1.24.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
@@ -23,9 +23,9 @@ jobs:
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: v1.60.1
version: v2.0.2
lint-jsonschema:
runs-on: ubuntu-latest
@@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v46
with:
files: website/versioned_docs/**

View File

@@ -11,16 +11,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}

View File

@@ -13,7 +13,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [1.22.x, 1.23.x]
go-version: [1.23.x, 1.24.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:

View File

@@ -1,30 +1,64 @@
# NOTE(@andreynering): The linters listed here are additions on top of
# those enabled by default:
#
# https://golangci-lint.run/usage/linters/#enabled-by-default
version: "2"
formatters:
enable:
- gofmt
- gofumpt
- goimports
- gci
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
gofumpt:
module-path: github.com/go-task/task/v3
goimports:
local-prefixes:
- github.com/go-task
gci:
sections:
- standard
- default
- prefix(github.com/go-task)
- localmodule
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters:
enable:
- depguard
- goimports
- gofmt
- gofumpt
- mirror
- misspell
linters-settings:
depguard:
rules:
main:
files:
- "$all"
- "!$test"
- "!**/errors/*.go"
deny:
- pkg: "errors"
desc: "Use github.com/go-task/task/v3/errors instead"
goimports:
local-prefixes: github.com/go-task
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- noctx
- paralleltest
- thelper
- tparallel
- usetesting
settings:
depguard:
rules:
main:
files:
- $all
- '!$test'
- '!**/errors/*.go'
deny:
- pkg: errors
desc: Use github.com/go-task/task/v3/errors instead
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$

View File

@@ -43,7 +43,7 @@ archives:
- completion/**/*
format_overrides:
- goos: windows
format: zip
formats: [zip]
release:
draft: true
@@ -57,7 +57,7 @@ checksum:
nfpms:
- vendor: Task
homepage: https://taskfile.dev
maintainer: Andrey Nering <andrey@nering.com.br>
maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go
license: MIT
conflicts:

View File

@@ -1,4 +1,8 @@
with-expecter: true
keeptree: true
case: underscore
output: ./internal/mocks
all: False
template: testify
filename: '{{base (trimSuffix ".go" .InterfaceFile)}}_mock.go'
packages:
github.com/go-task/task/v3/internal/fingerprint:
interfaces:
SourcesCheckable:
StatusCheckable:

2
.nvmrc
View File

@@ -1 +1 @@
18.20.4
22.14.0

4
.taskrc.yml Normal file
View File

@@ -0,0 +1,4 @@
experiments:
GENTLE_FORCE: 0
REMOTE_TASKFILES: 0
ENV_PRECEDENCE: 0

View File

@@ -1,11 +1,209 @@
# Changelog
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by
@vmaerten).
## v3.43.1 - 2025-04-21
- Significant improvements were made to the watcher. We migrated from
[watcher](https://github.com/radovskyb/watcher) to
[fsnotify](https://github.com/fsnotify/fsnotify). The former library used
polling, which means Task had a high CPU usage when watching too many files.
`fsnotify` uses proper the APIs from each operating system to watch files,
which means a much better performance. The default interval changed from 5
seconds to 100 milliseconds, because now it configures the wait time for
duplicated events, instead of the polling time (#2048 by @andreynering, #1508,
#985, #1179).
- The [Map Variables experiment](https://github.com/go-task/task/issues/1585)
was made generally available so you can now
[define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables)
(#1585, #1547, #2081 by @pd93).
- Wildcards can now
[match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072,
#2121 by @pd93).
- Added the ability to
[loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files).
This works the same way as looping over sources (#2151 by @sedyh).
- Added the ability to resolve variables when defining an include variable
(#2108, #2113 by @pd93).
- A few changes have been made to the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317)
(#1402, #2176 by @pd93):
- Cached files are now prioritized over remote ones.
- Added an `--expiry` flag which sets the TTL for a remote file cache. By
default the value will be 0 (caching disabled). If Task is running in
offline mode or fails to make a connection, it will fallback on the cache.
- `.taskrc` files can now be used from subdirectories and will be searched for
recursively up the file tree in the same way that Taskfiles are (#2159, #2166
by @pd93).
- The default taskfile (output when using the `--init` flag) is now an embedded
file in the binary instead of being stored in the code (#2112 by @pd93).
- Improved the way we report the Task version when using the `--version` flag or
`{{.TASK_VERSION}}` variable. This should now be more consistent and easier
for package maintainers to use (#2131 by @pd93).
- Fixed a bug where globstar (`**`) matching in `sources` only resolved the
first result (#2073, #2075 by @pd93).
- Fixed a bug where sorting tasks by "none" would use the default sorting
instead of leaving tasks in the order they were defined (#2124, #2125 by
@trulede).
- Fixed Fish completion on newer Fish versions (#2130 by @atusy).
- Fixed a bug where undefined/null variables resolved to an empty string instead
of `nil` (#1911, #2144 by @pd93).
- The `USER_WORKING_DIR` special now will now properly account for the `--dir`
(`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering).
- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).
- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).
#### Package API
- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now
uses the functional options pattern (#2085, #2147, #2148 by @pd93).
- The functional options for the
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
and
[`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by
@pd93).
- [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
no longer accepts a
[`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node).
Instead nodes are passed directly into the
[`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
method (#2169 by @pd93).
- [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
also now accepts a [`context.Context`](https://pkg.go.dev/context#Context)
(#2176 by @pd93).
## v3.42.1 - 2025-03-10
- Fixed a bug where some special variables caused a type error when used global
variables (#2106, #2107 by @pd93).
## v3.42.0 - 2025-03-08
- Made `--init` less verbose by default and respect `--silent` and `--verbose`
flags (#2009, #2011 by @HeCorr).
- `--init` now accepts a file name or directory as an argument (#2008, #2018 by
@HeCorr).
- Fix a bug where an HTTP node's location was being mutated incorrectly (#2007
by @jeongukjae).
- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by
@vmaerten).
- Use only the relevant checker (timestamp or checksum) to improve performance
(#2029, #2031 by @vmaerten).
- Print warnings when attempting to enable an inactive experiment or an active
experiment with an invalid value (#1979, #2049 by @pd93).
- Refactored the experiments package and added tests (#2049 by @pd93).
- Show allowed values when a variable with an enum is missing (#2027, #2052 by
@vmaerten).
- Refactored how snippets in error work and added tests (#2068 by @pd93).
- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by
@pd93).
- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand
`cmds` syntax were not considered valid (#2068 by @pd93).
- Refactored how task sorting functions work (#1798 by @pd93).
- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable
experiments (similar to `.env`) (#1982 by @vmaerten).
- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086
by @pd93).
- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).
- Fixed a bug where, when a dynamic variable is provided, even if it is not
used, all other variables become unavailable in the templating system within
the include (#2092 by @vmaerten).
#### Package API
Unlike our CLI tool,
[Task's package API is not currently stable](https://taskfile.dev/reference/package).
In an effort to ease the pain of breaking changes for our users, we will be
providing changelogs for our package API going forwards. The hope is that these
changes will provide a better long-term experience for our users and allow to
stabilize the API in the future. #121 now tracks this piece of work.
- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).
- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)
(#2011, ff8c913 by @HeCorr and @pd93)
- No longer accepts an `io.Writer` (output is now the caller's
responsibility).
- The path argument can now be a filename OR a directory.
- The function now returns the full path of the generated file.
- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)
now accepts a string instead of the arguments required to generate a snippet
(#2068 by @pd93).
- The caller is now expected to create the snippet themselves (see below).
- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
and related code moved from the `errors` package to the `taskfile` package
(#2068 by @pd93).
- Renamed `TaskMissingRequiredVars` to
[`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)
(#2052 by @vmaerten).
- Renamed `TaskNotAllowedVars` to
[`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)
(#2052 by @vmaerten).
- The
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
is now constructed using the functional options pattern (#2082 by @pd93).
- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082
by @pd93).
- Users are now expected to pass a custom debug/prompt functions into
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
if they want this functionality by using the new
[`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)
and
[`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)
functional options.
- Remove `Range` functions in the `taskfile/ast` package in favour of new
iterator functions (#1798 by @pd93).
- `ast.Call` was moved from the `taskfile/ast` package to the main `task`
package (#2084 by @pd93).
- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the
`task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).
- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were
moved from the `internal/compiler` package to the main `task` package (#2084
by @pd93).
## v3.41.0 - 2025-01-18
- Fixed an issue where dynamic variables were not properly logged in verbose
mode (#1920, #1921 by @mgbowman).
- Support `silent` for defer statements (#1877, #1879 by @danilobuerger).
- Added an option to exclude some tasks from being included (#1859 by
@vmaerten).
- Fixed an issue where a required variable was incorrectly handled in a template
function (#1950, #1962 by @vmaerten).
- Expose a new `TASK_DIR` special variable, which will contain the absolute path
of task directory. (#1959, #1961 by @vmaerten).
- Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by
@pd93, @GrahamDennis and @trim21).
- Refactored internal ordered map implementation to use
[github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap)
(#1797 by @pd93).
- Fixed a bug where variables defined at the task level were being ignored in
the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko)
- The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds`
(#1872 by @niklasr22).
- Updated [installation docs](https://taskfile.dev/installation) and added pip
installation method (#935, #1989 by @pd93).
- Fixed a bug where dynamic variables could not access environment variables
(#630, #1869 by @rohm1 and @pd93).
- Disable version check for use as an external library (#1938 by @leaanthony).
## v3.40.1 - 2024-12-06
- Fixed a security issue in `git-urls` by switching to the maintained fork `chainguard-dev/git-urls` (#1917 by
@AlekSi).
- Added missing `platforms` property to `cmds` that use `for` (#1915 by @dkarter).
- Added misspell linter to check for misspelled English words (#1883 by @christiandins).
- Fixed a security issue in `git-urls` by switching to the maintained fork
`chainguard-dev/git-urls` (#1917 by @AlekSi).
- Added missing `platforms` property to `cmds` that use `for` (#1915 by
@dkarter).
- Added misspell linter to check for misspelled English words (#1883 by
@christiandins).
## v3.40.0 - 2024-11-05
@@ -299,8 +497,8 @@
- Added the
[Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
as a draft (#1152, #1317 by @pd93).
- Improve performance of content checksumming on `sources:` by replacing md5 with
[XXH3](https://xxhash.com/) which is much faster. This is a soft breaking
- Improve performance of content checksumming on `sources:` by replacing md5
with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking
change because checksums will be invalidated when upgrading to this release
(#1325 by @ReillyBrogan).
@@ -447,8 +645,8 @@ it a go and let us know what you think via a
- Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007
by @pd93).
- Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93)
- Added environment variable `FORCE_COLOR` support to force color output.
Useful for environments without TTY (#1003 by @automation-stack)
- Added environment variable `FORCE_COLOR` support to force color output. Useful
for environments without TTY (#1003 by @automation-stack)
## v3.20.0 - 2023-01-14

View File

@@ -10,6 +10,18 @@
</p>
<p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p>
<h1>Gold Sponsors</h1>
<table>
<tr>
<td align="center" valign="middle">
<a target="_blank" href="https://devowl.io">
<img src="/website/static/img/devowl.io.svg" height="100px" title="devowl.io" />
</a>
</td>
</tr>
</table>
</div>

View File

@@ -18,6 +18,11 @@ tasks:
- task: lint
- task: test
run:
desc: Runs Task
cmds:
- go run ./cmd/task {{.CLI_ARGS}}
install:
desc: Installs Task
aliases: [i]
@@ -27,27 +32,41 @@ tasks:
- go install -v ./cmd/task
generate:
desc: Runs Mockery to create mocks
aliases: [gen, g]
desc: Runs all generate tasks
cmds:
- task: generate:mocks
- task: generate:fixtures
generate:mocks:
desc: Runs Mockery to create mocks
aliases: [gen:mocks, g:mocks]
deps: [install:mockery]
sources:
- "internal/fingerprint/checker.go"
generates:
- "internal/mocks/*.go"
cmds:
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name SourcesCheckable"
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name StatusCheckable"
- find . -type f -name *_mock.go -delete
- "{{.BIN}}/mockery"
generate:fixtures:
desc: Runs tests and generates golden fixture files
aliases: [gen:fixtures, g:fixtures]
cmds:
- find ./testdata -name '*.golden' -delete
- go test -update ./...
install:mockery:
desc: Installs mockgen; a tool to generate mock files
vars:
MOCKERY_VERSION: v2.24.0
MOCKERY_VERSION: v3.2.2
env:
GOBIN: "{{.BIN}}"
status:
- go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}}
cmds:
- go install github.com/vektra/mockery/v2@{{.MOCKERY_VERSION}}
- GOBIN="{{.BIN}}" go install github.com/vektra/mockery/v3@{{.MOCKERY_VERSION}}
mod:
desc: Downloads and tidy Go modules
@@ -79,6 +98,15 @@ tasks:
cmds:
- golangci-lint run --fix
format:
desc: Runs golangci-lint and formats any Go files
aliases: [fmt, f]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint fmt
sleepit:build:
desc: Builds the sleepit test helper
sources:
@@ -98,21 +126,23 @@ tasks:
test:
desc: Runs test suite
aliases: [t]
deps: [install]
sources:
- "**/*.go"
- "testdata/**/*"
cmds:
- go test {{catLines .GO_PACKAGES}}
vars:
GO_PACKAGES:
sh: go list ./...
- go test ./...
test:watch:
desc: Runs test suite with watch tests included
deps: [sleepit:build]
cmds:
- go test ./... -tags 'watch'
test:all:
desc: Runs test suite with signals and watch tests included
deps: [install, sleepit:build]
deps: [sleepit:build]
cmds:
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
vars:
GO_PACKAGES:
sh: go list ./...
- go test -tags 'signals watch' ./...
goreleaser:test:
desc: Tests release process without publishing
@@ -124,6 +154,22 @@ tasks:
cmds:
- go install github.com/goreleaser/goreleaser/v2@latest
gorelease:install:
desc: "Installs gorelease: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease"
status:
- command -v gorelease
cmds:
- go install golang.org/x/exp/cmd/gorelease@latest
api:check:
desc: Checks what changes have been made to the public API
deps: [gorelease:install]
vars:
LATEST:
sh: git describe --tags --abbrev=0
cmds:
- gorelease -base={{.LATEST}}
release:*:
desc: Prepare the project for a new release
summary: |
@@ -137,7 +183,7 @@ tasks:
- Push the commit/tag to the repository
- Create a GitHub release
To use the task, simply run "task release:<version>" where "<version>" is is one of:
To use the task, run "task release:<version>" where "<version>" is is one of:
- "major" - Bumps the major number
- "minor" - Bumps the minor number
@@ -176,11 +222,3 @@ tasks:
desc: Publish release to npm
cmds:
- npm publish --access=public
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
vars:
GO_PACKAGES:
sh: go list ./...
silent: true

View File

@@ -3,17 +3,44 @@ package args
import (
"strings"
"github.com/spf13/pflag"
"mvdan.cc/sh/v3/syntax"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/taskfile/ast"
)
// Get fetches the remaining arguments after CLI parsing and splits them into
// two groups: the arguments before the double dash (--) and the arguments after
// the double dash.
func Get() ([]string, string, error) {
args := pflag.Args()
doubleDashPos := pflag.CommandLine.ArgsLenAtDash()
if doubleDashPos == -1 {
return args, "", nil
}
var quotedCliArgs []string
for _, arg := range args[doubleDashPos:] {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return nil, "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
}
// Parse parses command line argument: tasks and global variables
func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
calls := []*ast.Call{}
globals := &ast.Vars{}
func Parse(args ...string) ([]*task.Call, *ast.Vars) {
calls := []*task.Call{}
globals := ast.NewVars()
for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, &ast.Call{Task: arg})
calls = append(calls, &task.Call{Task: arg})
continue
}

View File

@@ -6,20 +6,22 @@ import (
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/taskfile/ast"
)
func TestArgs(t *testing.T) {
t.Parallel()
tests := []struct {
Args []string
ExpectedCalls []*ast.Call
ExpectedCalls []*task.Call
ExpectedGlobals *ast.Vars
}{
{
Args: []string{"task-a", "task-b", "task-c"},
ExpectedCalls: []*ast.Call{
ExpectedCalls: []*task.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
@@ -27,81 +29,98 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
ExpectedCalls: []*ast.Call{
ExpectedCalls: []*task.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
},
ExpectedGlobals: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
"BAZ": {Value: "foo"},
ExpectedGlobals: ast.NewVars(
&ast.VarElement{
Key: "FOO",
Value: ast.Var{
Value: "bar",
},
[]string{"FOO", "BAR", "BAZ"},
),
},
},
&ast.VarElement{
Key: "BAR",
Value: ast.Var{
Value: "baz",
},
},
&ast.VarElement{
Key: "BAZ",
Value: ast.Var{
Value: "foo",
},
},
),
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []*ast.Call{
ExpectedCalls: []*task.Call{
{Task: "task-a"},
},
ExpectedGlobals: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"CONTENT": {Value: "with some spaces"},
ExpectedGlobals: ast.NewVars(
&ast.VarElement{
Key: "CONTENT",
Value: ast.Var{
Value: "with some spaces",
},
[]string{"CONTENT"},
),
},
},
),
},
{
Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []*ast.Call{
ExpectedCalls: []*task.Call{
{Task: "task-a"},
{Task: "task-b"},
},
ExpectedGlobals: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"FOO": {Value: "bar"},
ExpectedGlobals: ast.NewVars(
&ast.VarElement{
Key: "FOO",
Value: ast.Var{
Value: "bar",
},
[]string{"FOO"},
),
},
},
),
},
{
Args: nil,
ExpectedCalls: []*ast.Call{},
ExpectedCalls: []*task.Call{},
},
{
Args: []string{},
ExpectedCalls: []*ast.Call{},
ExpectedCalls: []*task.Call{},
},
{
Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []*ast.Call{},
ExpectedGlobals: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
ExpectedCalls: []*task.Call{},
ExpectedGlobals: ast.NewVars(
&ast.VarElement{
Key: "FOO",
Value: ast.Var{
Value: "bar",
},
[]string{"FOO", "BAR"},
),
},
},
&ast.VarElement{
Key: "BAR",
Value: ast.Var{
Value: "baz",
},
},
),
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
t.Parallel()
calls, globals := args.Parse(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls)
if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {
assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys())
assert.Equal(t, test.ExpectedGlobals.Values(), globals.Values())
assert.Equal(t, test.ExpectedGlobals, globals)
assert.Equal(t, test.ExpectedGlobals, globals)
}
})
}

View File

@@ -1,9 +1,11 @@
package ast
package task
import "github.com/go-task/task/v3/taskfile/ast"
// Call is the parameters to a task call
type Call struct {
Task string
Vars *Vars
Vars *ast.Vars
Silent bool
Indirect bool // True if the task was called by another task
}

View File

@@ -67,6 +67,10 @@ func release() error {
return err
}
if err := setVersionFile("internal/version/version.txt", version); err != nil {
return err
}
if err := setJSONVersion("package.json", version); err != nil {
return err
}
@@ -144,6 +148,10 @@ func changelog(version *semver.Version) error {
return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
}
func setVersionFile(fileName string, version *semver.Version) error {
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644)
}
func setJSONVersion(fileName string, version *semver.Version) error {
// Read the JSON file
b, err := os.ReadFile(fileName)

View File

@@ -4,20 +4,18 @@ import (
"context"
"fmt"
"os"
"strings"
"path/filepath"
"github.com/spf13/pflag"
"mvdan.cc/sh/v3/syntax"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -44,7 +42,7 @@ func main() {
}
func run() error {
logger := &logger.Logger{
log := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
@@ -55,11 +53,12 @@ func run() error {
return err
}
dir := flags.Dir
entrypoint := flags.Entrypoint
if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error())
}
if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
fmt.Println(version.GetVersionWithBuildInfo())
return nil
}
@@ -69,7 +68,7 @@ func run() error {
}
if flags.Experiments {
return experiments.List(logger)
return log.PrintExperiments()
}
if flags.Init {
@@ -77,9 +76,28 @@ func run() error {
if err != nil {
return err
}
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
args, _, err := args.Get()
if err != nil {
return err
}
path := wd
if len(args) > 0 {
name := args[0]
if filepathext.IsExtOnly(name) {
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
}
path = filepathext.SmartJoin(wd, name)
}
finalPath, err := task.InitTaskfile(path)
if err != nil {
return err
}
if !flags.Silent {
if flags.Verbose {
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
}
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
}
return nil
}
@@ -92,81 +110,29 @@ func run() error {
return nil
}
if flags.Global {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("task: Failed to get user home directory: %w", err)
}
dir = home
}
var taskSorter sort.TaskSorter
switch flags.TaskSort {
case "none":
taskSorter = &sort.Noop{}
case "alphanumeric":
taskSorter = &sort.AlphaNumeric{}
}
e := task.Executor{
Dir: dir,
Entrypoint: entrypoint,
Force: flags.Force,
ForceAll: flags.ForceAll,
Insecure: flags.Insecure,
Download: flags.Download,
Offline: flags.Offline,
Timeout: flags.Timeout,
Watch: flags.Watch,
Verbose: flags.Verbose,
Silent: flags.Silent,
AssumeYes: flags.AssumeYes,
Dry: flags.Dry || flags.Status,
Summary: flags.Summary,
Parallel: flags.Parallel,
Color: flags.Color,
Concurrency: flags.Concurrency,
Interval: flags.Interval,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
OutputStyle: flags.Output,
TaskSorter: taskSorter,
}
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil {
e := task.NewExecutor(
flags.WithFlags(),
task.WithVersionCheck(true),
)
if err := e.Setup(); err != nil {
return err
}
err := e.Setup()
if err != nil {
return err
}
if experiments.AnyVariables.Enabled {
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
}
// If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded
if flags.Download {
return nil
}
if flags.ClearCache {
cache, err := taskfile.NewCache(e.TempDir.Remote)
if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll)
cachePath := filepath.Join(e.TempDir.Remote, "remote")
return os.RemoveAll(cachePath)
}
listOptions := task.NewListOptions(
flags.List,
flags.ListAll,
flags.ListJson,
flags.NoStatus,
)
if listOptions.ShouldListTasks() {
if flags.Silent {
return e.ListTaskNames(flags.ListAll)
}
foundTasks, err := e.ListTasks(listOptions)
if err != nil {
return err
@@ -177,21 +143,16 @@ func run() error {
return nil
}
var (
calls []*ast.Call
globals *ast.Vars
)
tasksAndVars, cliArgs, err := getArgs()
// Parse the remaining arguments
argv, cliArgs, err := args.Get()
if err != nil {
return err
}
calls, globals = args.Parse(tasksAndVars...)
calls, globals := args.Parse(argv...)
// If there are no calls, run the default task instead
if len(calls) == 0 {
calls = append(calls, &ast.Call{Task: "default"})
calls = append(calls, &task.Call{Task: "default"})
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
@@ -213,24 +174,3 @@ func run() error {
return e.Run(ctx, calls...)
}
func getArgs() ([]string, string, error) {
var (
args = pflag.Args()
doubleDashPos = pflag.CommandLine.ArgsLenAtDash()
)
if doubleDashPos == -1 {
return args, "", nil
}
var quotedCliArgs []string
for _, arg := range args[doubleDashPos:] {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return nil, "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
}

38
cmd/tmp/main.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
if err := run(ctx); err != nil {
fmt.Println(ctx.Err())
fmt.Println(err)
}
}
func run(ctx context.Context) error {
req, err := http.NewRequest("GET", "https://taskfile.dev/schema.json", nil)
if err != nil {
fmt.Println(1)
return err
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
if ctx.Err() != nil {
fmt.Println(2)
return err
}
fmt.Println(3)
return err
}
defer resp.Body.Close()
return nil
}

View File

@@ -1,15 +1,15 @@
package compiler
package task
import (
"bytes"
"context"
"fmt"
"maps"
"os"
"path/filepath"
"strings"
"sync"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
@@ -36,16 +36,16 @@ func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {
return c.getVariables(nil, nil, true)
}
func (c *Compiler) GetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
return c.getVariables(t, call, true)
}
func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {
return c.getVariables(t, call, false)
}
func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
result := GetEnviron()
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := env.GetEnviron()
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
return nil, err
@@ -75,12 +75,12 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
return err
}
// If the variable is already set, we can set it and return
if newVar.Value != nil {
if newVar.Value != nil || newVar.Sh == nil {
result.Set(k, ast.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir)
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
if err != nil {
return err
}
@@ -103,36 +103,48 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
taskRangeFunc = getRangeFunc(dir)
}
if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
return nil, err
}
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
return nil, err
}
if t != nil {
if err := t.IncludeVars.Range(rangeFunc); err != nil {
for k, v := range c.TaskfileEnv.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
}
for k, v := range c.TaskfileVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
if t != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range t.IncludedTaskfileVars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
}
if t == nil || call == nil {
return result, nil
}
if err := call.Vars.Range(rangeFunc); err != nil {
return nil, err
for k, v := range call.Vars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
if err := t.Vars.Range(taskRangeFunc); err != nil {
return nil, err
for k, v := range t.Vars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
return result, nil
}
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) {
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
@@ -159,6 +171,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
Dir: dir,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
Env: e,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err)
@@ -170,7 +183,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
result = strings.TrimSuffix(result, "\n")
c.dynamicCache[*v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result)
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
return result, nil
}
@@ -183,7 +196,7 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil
}
func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {
allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
@@ -192,10 +205,21 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]strin
"TASK_VERSION": version.GetVersion(),
}
if t != nil {
maps.Copy(allVars, map[string]string{"TASK": t.Task, "TASKFILE": t.Location.Taskfile, "TASKFILE_DIR": filepath.Dir(t.Location.Taskfile)})
allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""
allVars["TASKFILE"] = ""
allVars["TASKFILE_DIR"] = ""
}
if call != nil {
maps.Copy(allVars, map[string]string{"ALIAS": call.Task})
allVars["ALIAS"] = call.Task
} else {
allVars["ALIAS"] = ""
}
return allVars, nil
}

View File

@@ -1,8 +1,25 @@
set GO_TASK_PROGNAME task
set -l GO_TASK_PROGNAME task
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# Check if the global task is requested
set -l global_task false
commandline --current-process | read --tokenize --list --local cmd_args
for arg in $cmd_args
if test "_$arg" = "_--"
break # ignore arguments to be passed to the task
end
if test "_$arg" = "_--global" -o "_$arg" = "_-g"
set global_task true
break
end
end
function __task_get_tasks --description "Prints all available tasks with their description"
# Read the list of tasks (and potential errors)
$GO_TASK_PROGNAME --list-all 2>&1 | read -lz rawOutput
if $global_task
$GO_TASK_PROGNAME --global --list-all
else
$GO_TASK_PROGNAME --list-all
end 2>&1 | read -lz rawOutput
# Return on non-zero exit code (for cases when there is no Taskfile found or etc.)
if test $status -ne 0

View File

@@ -2,36 +2,18 @@ package errors
import (
"bytes"
"embed"
"cmp"
"errors"
"fmt"
"regexp"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color"
"gopkg.in/yaml.v3"
)
//go:embed themes/*.xml
var embedded embed.FS
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
func init() {
r, err := embedded.Open("themes/task.xml")
if err != nil {
panic(err)
}
style, err := chroma.NewXMLStyle(r)
if err != nil {
panic(err)
}
styles.Register(style)
}
type (
TaskfileDecodeError struct {
Message string
@@ -39,15 +21,9 @@ type (
Line int
Column int
Tag string
Snippet TaskfileSnippet
Snippet string
Err error
}
TaskfileSnippet struct {
Lines []string
StartLine int
EndLine int
Padding int
}
)
func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
@@ -88,38 +64,44 @@ func (err *TaskfileDecodeError) Error() string {
}
}
fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
fmt.Fprint(buf, err.Snippet)
return buf.String()
}
// Print the snippet
maxLineNumberDigits := digits(err.Snippet.EndLine)
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
columnSpacer := strings.Repeat(" ", err.Column-1)
for i, line := range err.Snippet.Lines {
currentLine := err.Snippet.StartLine + i + 1
func (err *TaskfileDecodeError) Debug() string {
const indentWidth = 2
buf := &bytes.Buffer{}
fmt.Fprintln(buf, "TaskfileDecodeError:")
lineIndicator := " "
if currentLine == err.Line {
lineIndicator = ">"
}
columnIndicator := "^"
// Recursively loop through the error chain and print any details
var debug func(error, int)
debug = func(err error, indent int) {
indentStr := strings.Repeat(" ", indent*indentWidth)
// Print each line
lineIndicator = color.RedString(lineIndicator)
columnIndicator = color.RedString(columnIndicator)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
// Print the column indicator
if currentLine == err.Line {
fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
// Nothing left to unwrap
if err == nil {
fmt.Fprintf(buf, "%sEnd of chain\n", indentStr)
return
}
// If there are more lines to print, add a newline
if i < len(err.Snippet.Lines)-1 {
fmt.Fprintln(buf)
// Taskfile decode error
decodeErr := &TaskfileDecodeError{}
if errors.As(err, &decodeErr) {
fmt.Fprintf(buf, "%s%s (%s:%d:%d)\n",
indentStr,
cmp.Or(decodeErr.Message, "<no_message>"),
decodeErr.Location,
decodeErr.Line,
decodeErr.Column,
)
debug(errors.Unwrap(err), indent+1)
return
}
fmt.Fprintf(buf, "%s%s\n", indentStr, err)
debug(errors.Unwrap(err), indent+1)
}
debug(err, 0)
return buf.String()
}
@@ -141,23 +123,9 @@ func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
return err
}
func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError {
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.WriteString(string(b))
}
lines := strings.Split(buf.String(), "\n")
start := max(err.Line-1-padding, 0)
end := min(err.Line+padding, len(lines)-1)
func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError {
err.Location = location
err.Snippet = TaskfileSnippet{
Lines: lines[start:end],
StartLine: start,
EndLine: end,
Padding: padding,
}
err.Snippet = snippet
return err
}
@@ -168,12 +136,3 @@ func extractTypeErrorMessage(message string) string {
}
return message
}
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}

View File

@@ -8,6 +8,11 @@ const (
CodeUnknown // Used when no other exit code is appropriate
)
// TaskRC related exit codes
const (
CodeTaskRCNotFoundError int = iota + 50
)
// Taskfile related exit codes
const (
CodeTaskfileNotFound int = iota + 100

View File

@@ -141,21 +141,37 @@ func (err *TaskCancelledNoTerminalError) Code() int {
return CodeTaskCancelled
}
// TaskMissingRequiredVars is returned when a task is missing required variables.
type TaskMissingRequiredVars struct {
// TaskMissingRequiredVarsError is returned when a task is missing required variables.
type MissingVar struct {
Name string
AllowedValues []string
}
type TaskMissingRequiredVarsError struct {
TaskName string
MissingVars []string
MissingVars []MissingVar
}
func (err *TaskMissingRequiredVars) Error() string {
func (v MissingVar) String() string {
if len(v.AllowedValues) == 0 {
return v.Name
}
return fmt.Sprintf("%s (allowed values: %v)", v.Name, v.AllowedValues)
}
func (err *TaskMissingRequiredVarsError) Error() string {
var vars []string
for _, v := range err.MissingVars {
vars = append(vars, v.String())
}
return fmt.Sprintf(
`task: Task %q cancelled because it is missing required variables: %s`,
err.TaskName,
strings.Join(err.MissingVars, ", "),
)
strings.Join(vars, ", "))
}
func (err *TaskMissingRequiredVars) Code() int {
func (err *TaskMissingRequiredVarsError) Code() int {
return CodeTaskMissingRequiredVars
}
@@ -165,12 +181,12 @@ type NotAllowedVar struct {
Name string
}
type TaskNotAllowedVars struct {
type TaskNotAllowedVarsError struct {
TaskName string
NotAllowedVars []NotAllowedVar
}
func (err *TaskNotAllowedVars) Error() string {
func (err *TaskNotAllowedVarsError) Error() string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
@@ -181,6 +197,6 @@ func (err *TaskNotAllowedVars) Error() string {
return builder.String()
}
func (err *TaskNotAllowedVars) Code() int {
func (err *TaskNotAllowedVarsError) Code() int {
return CodeTaskNotAllowedVars
}

View File

@@ -155,19 +155,14 @@ func (err *TaskfileVersionCheckError) Code() int {
// TaskfileNetworkTimeoutError is returned when the user attempts to use a remote
// Taskfile but a network connection could not be established within the timeout.
type TaskfileNetworkTimeoutError struct {
URI string
Timeout time.Duration
CheckedCache bool
URI string
Timeout time.Duration
}
func (err *TaskfileNetworkTimeoutError) Error() string {
var cacheText string
if err.CheckedCache {
cacheText = " and no offline copy was found in the cache"
}
return fmt.Sprintf(
`task: Network connection timed out after %s while attempting to download Taskfile %q%s`,
err.Timeout, err.URI, cacheText,
`task: Network connection timed out after %s while attempting to download Taskfile %q`,
err.Timeout, err.URI,
)
}

20
errors/errors_taskrc.go Normal file
View File

@@ -0,0 +1,20 @@
package errors
import "fmt"
type TaskRCNotFoundError struct {
URI string
Walk bool
}
func (err TaskRCNotFoundError) Error() string {
var walkText string
if err.Walk {
walkText = " (or any of the parent directories)"
}
return fmt.Sprintf(`task: No Task config file found at %q%s`, err.URI, walkText)
}
func (err TaskRCNotFoundError) Code() int {
return CodeTaskRCNotFoundError
}

504
executor.go Normal file
View File

@@ -0,0 +1,504 @@
package task
import (
"context"
"io"
"os"
"sync"
"time"
"github.com/puzpuzpuz/xsync/v3"
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
)
type (
// An ExecutorOption is any type that can apply a configuration to an
// [Executor].
ExecutorOption interface {
ApplyToExecutor(*Executor)
}
// An Executor is used for processing Taskfile(s) and executing the task(s)
// within them.
Executor struct {
// Flags
Dir string
Entrypoint string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
Download bool
Offline bool
Timeout time.Duration
CacheExpiryDuration time.Duration
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
// I/O
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Internal
Taskfile *ast.Taskfile
Logger *logger.Logger
Compiler *Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.Sorter
UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool]
}
TempDir struct {
Remote string
Fingerprint string
}
)
// NewExecutor creates a new [Executor] and applies the given functional options
// to it.
func NewExecutor(opts ...ExecutorOption) *Executor {
e := &Executor{
Timeout: time.Second * 10,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Logger: nil,
Compiler: nil,
Output: nil,
OutputStyle: ast.Output{},
TaskSorter: sort.AlphaNumericWithRootTasksFirst,
UserWorkingDir: "",
fuzzyModel: nil,
concurrencySemaphore: nil,
taskCallCount: map[string]*int32{},
mkdirMutexMap: map[string]*sync.Mutex{},
executionHashes: map[string]context.Context{},
executionHashesMutex: sync.Mutex{},
}
e.Options(opts...)
return e
}
// Options loops through the given [ExecutorOption] functions and applies them
// to the [Executor].
func (e *Executor) Options(opts ...ExecutorOption) {
for _, opt := range opts {
opt.ApplyToExecutor(e)
}
}
// WithDir sets the working directory of the [Executor]. By default, the
// directory is set to the user's current working directory.
func WithDir(dir string) ExecutorOption {
return &dirOption{dir}
}
type dirOption struct {
dir string
}
func (o *dirOption) ApplyToExecutor(e *Executor) {
e.Dir = o.dir
}
// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By
// default, Task will search for one of the default Taskfiles in the given
// directory.
func WithEntrypoint(entrypoint string) ExecutorOption {
return &entrypointOption{entrypoint}
}
type entrypointOption struct {
entrypoint string
}
func (o *entrypointOption) ApplyToExecutor(e *Executor) {
e.Entrypoint = o.entrypoint
}
// WithTempDir sets the temporary directory that will be used by [Executor] for
// storing temporary files like checksums and cached remote files. By default,
// the temporary directory is set to the user's temporary directory.
func WithTempDir(tempDir TempDir) ExecutorOption {
return &tempDirOption{tempDir}
}
type tempDirOption struct {
tempDir TempDir
}
func (o *tempDirOption) ApplyToExecutor(e *Executor) {
e.TempDir = o.tempDir
}
// WithForce ensures that the [Executor] always runs a task, even when
// fingerprinting or prompts would normally stop it.
func WithForce(force bool) ExecutorOption {
return &forceOption{force}
}
type forceOption struct {
force bool
}
func (o *forceOption) ApplyToExecutor(e *Executor) {
e.Force = o.force
}
// WithForceAll ensures that the [Executor] always runs all tasks (including
// subtasks), even when fingerprinting or prompts would normally stop them.
func WithForceAll(forceAll bool) ExecutorOption {
return &forceAllOption{forceAll}
}
type forceAllOption struct {
forceAll bool
}
func (o *forceAllOption) ApplyToExecutor(e *Executor) {
e.ForceAll = o.forceAll
}
// WithInsecure allows the [Executor] to make insecure connections when reading
// remote taskfiles. By default, insecure connections are rejected.
func WithInsecure(insecure bool) ExecutorOption {
return &insecureOption{insecure}
}
type insecureOption struct {
insecure bool
}
func (o *insecureOption) ApplyToExecutor(e *Executor) {
e.Insecure = o.insecure
}
// WithDownload forces the [Executor] to download a fresh copy of the taskfile
// from the remote source.
func WithDownload(download bool) ExecutorOption {
return &downloadOption{download}
}
type downloadOption struct {
download bool
}
func (o *downloadOption) ApplyToExecutor(e *Executor) {
e.Download = o.download
}
// WithOffline stops the [Executor] from being able to make network connections.
// It will still be able to read local files and cached copies of remote files.
func WithOffline(offline bool) ExecutorOption {
return &offlineOption{offline}
}
type offlineOption struct {
offline bool
}
func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline
}
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
// default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption {
return &timeoutOption{timeout}
}
type timeoutOption struct {
timeout time.Duration
}
func (o *timeoutOption) ApplyToExecutor(e *Executor) {
e.Timeout = o.timeout
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is considered expired after 24 hours.
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}
type cacheExpiryDurationOption struct {
duration time.Duration
}
func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
r.CacheExpiryDuration = o.duration
}
// WithWatch tells the [Executor] to keep running in the background and watch
// for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered.
func WithWatch(watch bool) ExecutorOption {
return &watchOption{watch}
}
type watchOption struct {
watch bool
}
func (o *watchOption) ApplyToExecutor(e *Executor) {
e.Watch = o.watch
}
// WithVerbose tells the [Executor] to output more information about the tasks
// that are run.
func WithVerbose(verbose bool) ExecutorOption {
return &verboseOption{verbose}
}
type verboseOption struct {
verbose bool
}
func (o *verboseOption) ApplyToExecutor(e *Executor) {
e.Verbose = o.verbose
}
// WithSilent tells the [Executor] to suppress all output except for the output
// of the tasks that are run.
func WithSilent(silent bool) ExecutorOption {
return &silentOption{silent}
}
type silentOption struct {
silent bool
}
func (o *silentOption) ApplyToExecutor(e *Executor) {
e.Silent = o.silent
}
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
func WithAssumeYes(assumeYes bool) ExecutorOption {
return &assumeYesOption{assumeYes}
}
type assumeYesOption struct {
assumeYes bool
}
func (o *assumeYesOption) ApplyToExecutor(e *Executor) {
e.AssumeYes = o.assumeYes
}
// WithAssumeTerm is used for testing purposes to simulate a terminal.
func WithAssumeTerm(assumeTerm bool) ExecutorOption {
return &assumeTermOption{assumeTerm}
}
type assumeTermOption struct {
assumeTerm bool
}
func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
e.AssumeTerm = o.assumeTerm
}
// WithDry tells the [Executor] to output the commands that would be run without
// actually running them.
func WithDry(dry bool) ExecutorOption {
return &dryOption{dry}
}
type dryOption struct {
dry bool
}
func (o *dryOption) ApplyToExecutor(e *Executor) {
e.Dry = o.dry
}
// WithSummary tells the [Executor] to output a summary of the given tasks
// instead of running them.
func WithSummary(summary bool) ExecutorOption {
return &summaryOption{summary}
}
type summaryOption struct {
summary bool
}
func (o *summaryOption) ApplyToExecutor(e *Executor) {
e.Summary = o.summary
}
// WithParallel tells the [Executor] to run tasks given in the same call in
// parallel.
func WithParallel(parallel bool) ExecutorOption {
return &parallelOption{parallel}
}
type parallelOption struct {
parallel bool
}
func (o *parallelOption) ApplyToExecutor(e *Executor) {
e.Parallel = o.parallel
}
// WithColor tells the [Executor] whether or not to output using colorized
// strings.
func WithColor(color bool) ExecutorOption {
return &colorOption{color}
}
type colorOption struct {
color bool
}
func (o *colorOption) ApplyToExecutor(e *Executor) {
e.Color = o.color
}
// WithConcurrency sets the maximum number of tasks that the [Executor] can run
// in parallel.
func WithConcurrency(concurrency int) ExecutorOption {
return &concurrencyOption{concurrency}
}
type concurrencyOption struct {
concurrency int
}
func (o *concurrencyOption) ApplyToExecutor(e *Executor) {
e.Concurrency = o.concurrency
}
// WithInterval sets the interval at which the [Executor] will wait for
// duplicated events before running a task.
func WithInterval(interval time.Duration) ExecutorOption {
return &intervalOption{interval}
}
type intervalOption struct {
interval time.Duration
}
func (o *intervalOption) ApplyToExecutor(e *Executor) {
e.Interval = o.interval
}
// WithOutputStyle sets the output style of the [Executor]. By default, the
// output style is set to the style defined in the Taskfile.
func WithOutputStyle(outputStyle ast.Output) ExecutorOption {
return &outputStyleOption{outputStyle}
}
type outputStyleOption struct {
outputStyle ast.Output
}
func (o *outputStyleOption) ApplyToExecutor(e *Executor) {
e.OutputStyle = o.outputStyle
}
// WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By
// default, the sorter is set to sort tasks alphabetically, but with tasks with
// no namespace (in the root Taskfile) first.
func WithTaskSorter(sorter sort.Sorter) ExecutorOption {
return &taskSorterOption{sorter}
}
type taskSorterOption struct {
sorter sort.Sorter
}
func (o *taskSorterOption) ApplyToExecutor(e *Executor) {
e.TaskSorter = o.sorter
}
// WithStdin sets the [Executor]'s standard input [io.Reader].
func WithStdin(stdin io.Reader) ExecutorOption {
return &stdinOption{stdin}
}
type stdinOption struct {
stdin io.Reader
}
func (o *stdinOption) ApplyToExecutor(e *Executor) {
e.Stdin = o.stdin
}
// WithStdout sets the [Executor]'s standard output [io.Writer].
func WithStdout(stdout io.Writer) ExecutorOption {
return &stdoutOption{stdout}
}
type stdoutOption struct {
stdout io.Writer
}
func (o *stdoutOption) ApplyToExecutor(e *Executor) {
e.Stdout = o.stdout
}
// WithStderr sets the [Executor]'s standard error [io.Writer].
func WithStderr(stderr io.Writer) ExecutorOption {
return &stderrOption{stderr}
}
type stderrOption struct {
stderr io.Writer
}
func (o *stderrOption) ApplyToExecutor(e *Executor) {
e.Stderr = o.stderr
}
// WithIO sets the [Executor]'s standard input, output, and error to the same
// [io.ReadWriter].
func WithIO(rw io.ReadWriter) ExecutorOption {
return &ioOption{rw}
}
type ioOption struct {
rw io.ReadWriter
}
func (o *ioOption) ApplyToExecutor(e *Executor) {
e.Stdin = o.rw
e.Stdout = o.rw
e.Stderr = o.rw
}
// WithVersionCheck tells the [Executor] whether or not to check the version of
func WithVersionCheck(enableVersionCheck bool) ExecutorOption {
return &versionCheckOption{enableVersionCheck}
}
type versionCheckOption struct {
enableVersionCheck bool
}
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
e.EnableVersionCheck = o.enableVersionCheck
}

939
executor_test.go Normal file
View File

@@ -0,0 +1,939 @@
package task_test
import (
"bytes"
"cmp"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/sebdah/goldie/v2"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
type (
// A ExecutorTestOption is a function that configures an [ExecutorTest].
ExecutorTestOption interface {
applyToExecutorTest(*ExecutorTest)
}
// A ExecutorTest is a test wrapper around a [task.Executor] to make it easy
// to write tests for tasks. See [NewExecutorTest] for information on
// creating and running ExecutorTests. These tests use fixture files to
// assert whether the result of a task is correct. If Task's behavior has
// been changed, the fixture files can be updated by running `task
// gen:fixtures`.
ExecutorTest struct {
TaskTest
task string
vars map[string]any
input string
executorOpts []task.ExecutorOption
wantSetupError bool
wantRunError bool
wantStatusError bool
}
)
// NewExecutorTest sets up a new [task.Executor] with the given options and runs
// a task with the given [ExecutorTestOption]s. The output of the task is
// written to a set of fixture files depending on the configuration of the test.
func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
t.Helper()
tt := &ExecutorTest{
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
},
}
// Apply the functional options
for _, opt := range opts {
opt.applyToExecutorTest(tt)
}
// Enable any experiments that have been set
for x, v := range tt.experiments {
prev := *x
*x = experiments.Experiment{
Name: prev.Name,
AllowedValues: []int{v},
Value: v,
}
t.Cleanup(func() {
*x = prev
})
}
tt.run(t)
}
// Functional options
// WithInput tells the test to create a reader with the given input. This can be
// used to simulate user input when a task requires it.
func WithInput(input string) ExecutorTestOption {
return &inputTestOption{input}
}
type inputTestOption struct {
input string
}
func (opt *inputTestOption) applyToExecutorTest(t *ExecutorTest) {
t.input = opt.input
}
// WithRunError tells the test to expect an error during the run phase of the
// task execution. A fixture will be created with the output of any errors.
func WithRunError() ExecutorTestOption {
return &runErrorTestOption{}
}
type runErrorTestOption struct{}
func (opt *runErrorTestOption) applyToExecutorTest(t *ExecutorTest) {
t.wantRunError = true
}
// WithStatusError tells the test to make an additional call to
// [task.Executor.Status] after the task has been run. A fixture will be created
// with the output of any errors.
func WithStatusError() ExecutorTestOption {
return &statusErrorTestOption{}
}
type statusErrorTestOption struct{}
func (opt *statusErrorTestOption) applyToExecutorTest(t *ExecutorTest) {
t.wantStatusError = true
}
// Helpers
// writeFixtureErrRun is a wrapper for writing the output of an error during the
// run phase of the task to a fixture file.
func (tt *ExecutorTest) writeFixtureErrRun(
t *testing.T,
g *goldie.Goldie,
err error,
) {
t.Helper()
tt.writeFixture(t, g, "err-run", []byte(err.Error()))
}
// writeFixtureStatus is a wrapper for writing the output of an error when
// making an additional call to [task.Executor.Status] to a fixture file.
func (tt *ExecutorTest) writeFixtureStatus(
t *testing.T,
g *goldie.Goldie,
status string,
) {
t.Helper()
tt.writeFixture(t, g, "err-status", []byte(status))
}
// run is the main function for running the test. It sets up the task executor,
// runs the task, and writes the output to a fixture file.
func (tt *ExecutorTest) run(t *testing.T) {
t.Helper()
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
opts := append(
tt.executorOpts,
task.WithStdout(&buf),
task.WithStderr(&buf),
)
// If the test has input, create a reader for it and add it to the
// executor options
if tt.input != "" {
var reader bytes.Buffer
reader.WriteString(tt.input)
opts = append(opts, task.WithStdin(&reader))
}
// Set up the task executor
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
// Create the task call
vars := ast.NewVars()
for key, value := range tt.vars {
vars.Set(key, ast.Var{Value: value})
}
call := &task.Call{
Task: tt.task,
Vars: vars,
}
// Run the task and check for errors
ctx := context.Background()
if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err)
tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
// If the status flag is set, run the status check
if tt.wantStatusError {
if err := e.Status(ctx, call); err != nil {
tt.writeFixtureStatus(t, g, err.Error())
}
}
tt.writeFixtureBuffer(t, g, buf)
}
// Run the test (with a name if it has one)
if tt.name != "" {
t.Run(tt.name, f)
} else {
f(t)
}
}
func TestEmptyTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/empty_task"),
),
)
}
func TestEmptyTaskfile(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/empty_taskfile"),
),
WithSetupError(),
WithPostProcessFn(PPRemoveAbsolutePaths),
)
}
func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os")
NewExecutorTest(t,
WithName("env precedence disabled"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
)
NewExecutorTest(t,
WithName("env precedence enabled"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
WithExperiment(&experiments.EnvPrecedence, 1),
)
}
func TestVars(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
)
}
func TestRequires(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("required var missing"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var ok"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var"),
WithVar("FOO", "bar"),
)
NewExecutorTest(t,
WithName("fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithVar("ENV", "dev"),
WithVar("FOO", "bar"),
WithRunError(),
)
NewExecutorTest(t,
WithName("passes validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-dynamic"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("require before compile"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("require-before-compile"),
WithRunError(),
)
NewExecutorTest(t,
WithName("var defined in task"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("var-defined-in-task"),
)
}
// TODO: mock fs
func TestSpecialVars(t *testing.T) {
t.Parallel()
const dir = "testdata/special_vars"
const subdir = "testdata/special_vars/subdir"
tests := []string{
// Root
"print-task",
"print-root-dir",
"print-taskfile",
"print-taskfile-dir",
"print-task-dir",
// Included
"included:print-task",
"included:print-root-dir",
"included:print-taskfile",
"included:print-taskfile-dir",
}
for _, dir := range []string{dir, subdir} {
for _, test := range tests {
NewExecutorTest(t,
WithName(fmt.Sprintf("%s-%s", dir, test)),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
task.WithVersionCheck(true),
),
WithTask(test),
WithPostProcessFn(PPRemoveAbsolutePaths),
)
}
}
}
func TestConcurrency(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/concurrency"),
task.WithConcurrency(1),
),
WithPostProcessFn(PPSortedLines),
)
}
func TestParams(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/params"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
)
}
func TestDeps(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/deps"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
)
}
// TODO: mock fs
func TestStatus(t *testing.T) {
t.Parallel()
const dir = "testdata/status"
files := []string{
"foo.txt",
"bar.txt",
"baz.txt",
}
for _, f := range files {
path := filepathext.SmartJoin(dir, f)
_ = os.Remove(path)
if _, err := os.Stat(path); err == nil {
t.Errorf("File should not exist: %v", err)
}
}
// gen-foo creates foo.txt, and will always fail it's status check.
NewExecutorTest(t,
WithName("run gen-foo 1 silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-foo"),
)
// gen-foo creates bar.txt, and will pass its status-check the 3. time it
// is run. It creates bar.txt, but also lists it as its source. So, the checksum
// for the file won't match before after the second run as we the file
// only exists after the first run.
NewExecutorTest(t,
WithName("run gen-bar 1 silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
)
// gen-silent-baz is marked as being silent, and should only produce output
// if e.Verbose is set to true.
NewExecutorTest(t,
WithName("run gen-baz silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-silent-baz"),
)
for _, f := range files {
if _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil {
t.Errorf("File should exist: %v", err)
}
}
// Run gen-bar a second time to produce a checksum file that matches bar.txt
NewExecutorTest(t,
WithName("run gen-bar 2 silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
)
// Run gen-bar a third time, to make sure we've triggered the status check.
NewExecutorTest(t,
WithName("run gen-bar 3 silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
)
// Now, let's remove source file, and run the task again to to prepare
// for the next test.
err := os.Remove(filepathext.SmartJoin(dir, "bar.txt"))
require.NoError(t, err)
NewExecutorTest(t,
WithName("run gen-bar 4 silent"),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
)
// all: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 2"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-foo"),
)
// status: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 3"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-foo"),
)
// sources: not up-to-date
NewExecutorTest(t,
WithName("run gen-bar 5"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-bar"),
)
// all: up-to-date
NewExecutorTest(t,
WithName("run gen-bar 6"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-bar"),
)
// sources: not up-to-date, no output produced.
NewExecutorTest(t,
WithName("run gen-baz 2"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-silent-baz"),
)
// up-to-date, no output produced
NewExecutorTest(t,
WithName("run gen-baz 3"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-silent-baz"),
)
// up-to-date, output produced due to Verbose mode.
NewExecutorTest(t,
WithName("run gen-baz 4 verbose"),
WithExecutorOptions(
task.WithDir(dir),
task.WithVerbose(true),
),
WithTask("gen-silent-baz"),
WithPostProcessFn(PPRemoveAbsolutePaths),
)
}
func TestPrecondition(t *testing.T) {
t.Parallel()
const dir = "testdata/precondition"
NewExecutorTest(t,
WithName("a precondition has been met"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("a precondition was not met"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in dependency fails the task"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("depends_on_impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in cmd fails the task"),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("executes_failing_task_as_cmd"),
WithRunError(),
)
}
func TestAlias(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithTask("f"),
)
NewExecutorTest(t,
WithName("duplicate alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithTask("x"),
WithRunError(),
)
NewExecutorTest(t,
WithName("alias summary"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
task.WithSummary(true),
),
WithTask("f"),
)
}
func TestLabel(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("up to date"),
WithExecutorOptions(
task.WithDir("testdata/label_uptodate"),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
task.WithSummary(true),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("status"),
WithExecutorOptions(
task.WithDir("testdata/label_status"),
),
WithTask("foo"),
WithStatusError(),
)
NewExecutorTest(t,
WithName("var"),
WithExecutorOptions(
task.WithDir("testdata/label_var"),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
),
WithTask("foo"),
)
}
func TestPromptInSummary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
wantError bool
}{
{"test short approval", "y\n", false},
{"test long approval", "yes\n", false},
{"test uppercase approval", "Y\n", false},
{"test stops task", "n\n", true},
{"test junk value stops task", "foobar\n", true},
{"test Enter stops task", "\n", true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
opts := []ExecutorTestOption{
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
WithInput(test.input),
}
if test.wantError {
opts = append(opts, WithRunError())
}
NewExecutorTest(t, opts...)
})
}
}
func TestPromptWithIndirectTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("bar"),
WithInput("y\n"),
)
}
func TestPromptAssumeYes(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("--yes flag should skip prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
task.WithAssumeYes(true),
),
WithTask("foo"),
WithInput("\n"),
)
NewExecutorTest(t,
WithName("task should raise errors.TaskCancelledError"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
WithInput("\n"),
WithRunError(),
)
}
func TestForCmds(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wantErr bool
}{
{name: "loop-explicit"},
{name: "loop-matrix"},
{name: "loop-matrix-ref"},
{
name: "loop-matrix-ref-error",
wantErr: true,
},
{name: "loop-sources"},
{name: "loop-sources-glob"},
{name: "loop-generates"},
{name: "loop-generates-glob"},
{name: "loop-vars"},
{name: "loop-vars-sh"},
{name: "loop-task"},
{name: "loop-task-as"},
{name: "loop-different-tasks"},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/for/cmds"),
task.WithSilent(true),
task.WithForce(true),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
}
if test.wantErr {
opts = append(opts, WithRunError())
}
NewExecutorTest(t, opts...)
}
}
func TestForDeps(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wantErr bool
}{
{name: "loop-explicit"},
{name: "loop-matrix"},
{name: "loop-matrix-ref"},
{
name: "loop-matrix-ref-error",
wantErr: true,
},
{name: "loop-sources"},
{name: "loop-sources-glob"},
{name: "loop-generates"},
{name: "loop-generates-glob"},
{name: "loop-vars"},
{name: "loop-vars-sh"},
{name: "loop-task"},
{name: "loop-task-as"},
{name: "loop-different-tasks"},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/for/deps"),
task.WithSilent(true),
task.WithForce(true),
// Force output of each dep to be grouped together to prevent interleaving
task.WithOutputStyle(ast.Output{Name: "group"}),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithPostProcessFn(PPSortedLines),
}
if test.wantErr {
opts = append(opts, WithRunError())
}
NewExecutorTest(t, opts...)
}
}
func TestReference(t *testing.T) {
t.Parallel()
tests := []struct {
name string
call string
}{
{
name: "reference in command",
call: "ref-cmd",
},
{
name: "reference in dependency",
call: "ref-dep",
},
{
name: "reference using templating resolver",
call: "ref-resolver",
},
{
name: "reference using templating resolver and dynamic var",
call: "ref-resolver-sh",
},
}
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/var_references"),
task.WithSilent(true),
task.WithForce(true),
),
WithTask(cmp.Or(test.call, "default")),
)
}
}
func TestVarInheritance(t *testing.T) {
enableExperimentForTest(t, &experiments.EnvPrecedence, 1)
tests := []struct {
name string
call string
}{
{name: "shell"},
{name: "entrypoint-global-dotenv"},
{name: "entrypoint-global-vars"},
// We can't send env vars to a called task, so the env var is not overridden
{name: "entrypoint-task-call-vars"},
// Dotenv doesn't set variables
{name: "entrypoint-task-call-dotenv"},
{name: "entrypoint-task-call-task-vars"},
// Dotenv doesn't set variables
{name: "entrypoint-task-dotenv"},
{name: "entrypoint-task-vars"},
// {
// // Dotenv not currently allowed in included taskfiles
// name: "included-global-dotenv",
// want: "included-global-dotenv\nincluded-global-dotenv\n",
// },
{
name: "included-global-vars",
call: "included",
},
{
// We can't send env vars to a called task, so the env var is not overridden
name: "included-task-call-vars",
call: "included",
},
{
// Dotenv doesn't set variables
// Dotenv not currently allowed in included taskfiles (but doesn't error in a task)
name: "included-task-call-dotenv",
call: "included",
},
{
name: "included-task-call-task-vars",
call: "included",
},
{
// Dotenv doesn't set variables
// Somehow dotenv is working here!
name: "included-task-dotenv",
call: "included",
},
{
name: "included-task-vars",
call: "included",
},
}
t.Setenv("VAR", "shell")
t.Setenv("ENV", "shell")
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithExecutorOptions(
task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
task.WithSilent(true),
task.WithForce(true),
),
WithTask(cmp.Or(test.call, "default")),
)
}
}

35
experiments/errors.go Normal file
View File

@@ -0,0 +1,35 @@
package experiments
import (
"fmt"
"strconv"
"strings"
"github.com/go-task/task/v3/internal/slicesext"
)
type InvalidValueError struct {
Name string
AllowedValues []int
Value int
}
func (err InvalidValueError) Error() string {
return fmt.Sprintf(
"task: Experiment %q has an invalid value %q (allowed values: %s)",
err.Name,
err.Value,
strings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), ", "),
)
}
type InactiveError struct {
Name string
}
func (err InactiveError) Error() string {
return fmt.Sprintf(
"task: Experiment %q is inactive and cannot be enabled",
err.Name,
)
}

67
experiments/experiment.go Normal file
View File

@@ -0,0 +1,67 @@
package experiments
import (
"fmt"
"slices"
"strconv"
"github.com/go-task/task/v3/taskrc/ast"
)
type Experiment struct {
Name string // The name of the experiment.
AllowedValues []int // The values that can enable this experiment.
Value int // The version of the experiment that is enabled.
}
// New creates a new experiment with the given name and sets the values that can
// enable it.
func New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment {
var value int
if config != nil {
value = config.Experiments[xName]
}
if value == 0 {
value, _ = strconv.Atoi(getEnv(xName))
}
x := Experiment{
Name: xName,
AllowedValues: allowedValues,
Value: value,
}
xList = append(xList, x)
return x
}
func (x Experiment) Enabled() bool {
return slices.Contains(x.AllowedValues, x.Value)
}
func (x Experiment) Active() bool {
return len(x.AllowedValues) > 0
}
func (x Experiment) Valid() error {
if !x.Active() && x.Value != 0 {
return &InactiveError{
Name: x.Name,
}
}
if !x.Enabled() && x.Value != 0 {
return &InvalidValueError{
Name: x.Name,
AllowedValues: x.AllowedValues,
Value: x.Value,
}
}
return nil
}
func (x Experiment) String() string {
if x.Enabled() {
return fmt.Sprintf("on (%d)", x.Value)
}
return "off"
}

View File

@@ -0,0 +1,140 @@
package experiments_test
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskrc/ast"
)
func TestNew(t *testing.T) {
const (
exampleExperiment = "EXAMPLE"
exampleExperimentEnv = "TASK_X_EXAMPLE"
)
tests := []struct {
name string
config *ast.TaskRC
allowedValues []int
env int
wantEnabled bool
wantActive bool
wantValid error
wantValue int
}{
{
name: `[] allowed, env=""`,
wantEnabled: false,
wantActive: false,
},
{
name: `[] allowed, env="1"`,
env: 1,
wantEnabled: false,
wantActive: false,
wantValid: &experiments.InactiveError{
Name: exampleExperiment,
},
wantValue: 1,
},
{
name: `[1] allowed, env=""`,
allowedValues: []int{1},
wantEnabled: false,
wantActive: true,
},
{
name: `[1] allowed, env="1"`,
allowedValues: []int{1},
env: 1,
wantEnabled: true,
wantActive: true,
wantValue: 1,
},
{
name: `[1] allowed, env="2"`,
allowedValues: []int{1},
env: 2,
wantEnabled: false,
wantActive: true,
wantValid: &experiments.InvalidValueError{
Name: exampleExperiment,
AllowedValues: []int{1},
Value: 2,
},
wantValue: 2,
},
{
name: `[1, 2] allowed, env="1"`,
allowedValues: []int{1, 2},
env: 1,
wantEnabled: true,
wantActive: true,
wantValue: 1,
},
{
name: `[1, 2] allowed, env="1"`,
allowedValues: []int{1, 2},
env: 2,
wantEnabled: true,
wantActive: true,
wantValue: 2,
},
{
name: `[1] allowed, config="1"`,
config: &ast.TaskRC{
Experiments: map[string]int{
exampleExperiment: 1,
},
},
allowedValues: []int{1},
wantEnabled: true,
wantActive: true,
wantValue: 1,
},
{
name: `[1] allowed, config="2"`,
config: &ast.TaskRC{
Experiments: map[string]int{
exampleExperiment: 2,
},
},
allowedValues: []int{1},
wantEnabled: false,
wantActive: true,
wantValid: &experiments.InvalidValueError{
Name: exampleExperiment,
AllowedValues: []int{1},
Value: 2,
},
wantValue: 2,
},
{
name: `[1, 2] allowed, env="1", config="2"`,
config: &ast.TaskRC{
Experiments: map[string]int{
exampleExperiment: 2,
},
},
allowedValues: []int{1, 2},
env: 1,
wantEnabled: true,
wantActive: true,
wantValue: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.env))
x := experiments.New(exampleExperiment, tt.config, tt.allowedValues...)
assert.Equal(t, exampleExperiment, x.Name)
assert.Equal(t, tt.wantEnabled, x.Enabled())
assert.Equal(t, tt.wantActive, x.Active())
assert.Equal(t, tt.wantValid, x.Valid())
assert.Equal(t, tt.wantValue, x.Value)
})
}
}

View File

@@ -0,0 +1,91 @@
package experiments
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
"github.com/go-task/task/v3/taskrc"
)
const envPrefix = "TASK_X_"
// Active experiments.
var (
GentleForce Experiment
RemoteTaskfiles Experiment
EnvPrecedence Experiment
)
// Inactive experiments. These are experiments that cannot be enabled, but are
// preserved for error handling.
var (
AnyVariables Experiment
MapVariables Experiment
)
// An internal list of all the initialized experiments used for iterating.
var xList []Experiment
func Parse(dir string) {
// Read any .env files
readDotEnv(dir)
// Create a node for the Task config reader
node, _ := taskrc.NewNode("", dir)
// Read the Task config file
reader := taskrc.NewReader()
config, _ := reader.Read(node)
// Initialize the experiments
GentleForce = New("GENTLE_FORCE", config, 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)
EnvPrecedence = New("ENV_PRECEDENCE", config, 1)
AnyVariables = New("ANY_VARIABLES", config)
MapVariables = New("MAP_VARIABLES", config)
}
// Validate checks if any experiments have been enabled while being inactive.
// If one is found, the function returns an error.
func Validate() error {
for _, x := range List() {
if err := x.Valid(); err != nil {
return err
}
}
return nil
}
func List() []Experiment {
return xList
}
func getEnv(xName string) string {
envName := fmt.Sprintf("%s%s", envPrefix, xName)
return os.Getenv(envName)
}
func getFilePath(filename, dir string) string {
if dir != "" {
return filepath.Join(dir, filename)
}
return filename
}
func readDotEnv(dir string) {
env, err := godotenv.Read(getFilePath(".env", dir))
if err != nil {
return
}
// If the env var is an experiment, set it.
for key, value := range env {
if strings.HasPrefix(key, envPrefix) {
os.Setenv(key, value)
}
}
}

220
formatter_test.go Normal file
View File

@@ -0,0 +1,220 @@
package task_test
import (
"bytes"
"path/filepath"
"testing"
"github.com/sebdah/goldie/v2"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast"
)
type (
// A FormatterTestOption is a function that configures an [FormatterTest].
FormatterTestOption interface {
applyToFormatterTest(*FormatterTest)
}
// A FormatterTest is a test wrapper around a [task.Executor] to make it
// easy to write tests for the task formatter. See [NewFormatterTest] for
// information on creating and running FormatterTests. These tests use
// fixture files to assert whether the result of the output is correct. If
// Task's behavior has been changed, the fixture files can be updated by
// running `task gen:fixtures`.
FormatterTest struct {
TaskTest
task string
vars map[string]any
executorOpts []task.ExecutorOption
listOptions task.ListOptions
wantSetupError bool
wantListError bool
}
)
// NewFormatterTest sets up a new [task.Executor] with the given options and
// runs a task with the given [FormatterTestOption]s. The output of the task is
// written to a set of fixture files depending on the configuration of the test.
func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
t.Helper()
tt := &FormatterTest{
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
},
}
// Apply the functional options
for _, opt := range opts {
opt.applyToFormatterTest(tt)
}
// Enable any experiments that have been set
for x, v := range tt.experiments {
prev := *x
*x = experiments.Experiment{
Name: prev.Name,
AllowedValues: []int{v},
Value: v,
}
t.Cleanup(func() {
*x = prev
})
}
tt.run(t)
}
// Functional options
// WithListOptions sets the list options for the formatter.
func WithListOptions(opts task.ListOptions) FormatterTestOption {
return &listOptionsTestOption{opts}
}
type listOptionsTestOption struct {
listOptions task.ListOptions
}
func (opt *listOptionsTestOption) applyToFormatterTest(t *FormatterTest) {
t.listOptions = opt.listOptions
}
// WithListError tells the test to expect an error when running the formatter.
// A fixture will be created with the output of any errors.
func WithListError() FormatterTestOption {
return &listErrorTestOption{}
}
type listErrorTestOption struct{}
func (opt *listErrorTestOption) applyToFormatterTest(t *FormatterTest) {
t.wantListError = true
}
// Helpers
// writeFixtureErrList is a wrapper for writing the output of an error when
// running the formatter to a fixture file.
func (tt *FormatterTest) writeFixtureErrList(
t *testing.T,
g *goldie.Goldie,
err error,
) {
t.Helper()
tt.writeFixture(t, g, "err-list", []byte(err.Error()))
}
// run is the main function for running the test. It sets up the task executor,
// runs the task, and writes the output to a fixture file.
func (tt *FormatterTest) run(t *testing.T) {
t.Helper()
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
opts := append(
tt.executorOpts,
task.WithStdout(&buf),
task.WithStderr(&buf),
)
// Set up the task executor
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
// Create the task call
vars := ast.NewVars()
for key, value := range tt.vars {
vars.Set(key, ast.Var{Value: value})
}
// Run the formatter and check for errors
if _, err := e.ListTasks(tt.listOptions); tt.wantListError {
require.Error(t, err)
tt.writeFixtureErrList(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
tt.writeFixtureBuffer(t, g, buf)
}
// Run the test (with a name if it has one)
if tt.name != "" {
t.Run(tt.name, f)
} else {
f(t)
}
}
func TestNoLabelInList(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/label_list"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
)
}
// task -al case 1: listAll list all tasks
func TestListAllShowsNoDesc(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{
ListAllTasks: true,
}),
)
}
// task -al case 2: !listAll list some tasks (only those with desc)
func TestListCanListDescOnly(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
)
}
func TestListDescInterpolation(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/list_desc_interpolation"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
)
}

50
go.mod
View File

@@ -1,60 +1,60 @@
module github.com/go-task/task/v3
go 1.22.0
go 1.23.0
require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.14.0
github.com/alecthomas/chroma/v2 v2.16.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0
github.com/go-git/go-billy/v5 v5.6.0
github.com/go-git/go-git/v5 v5.12.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.0
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.1.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.6
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.0
github.com/radovskyb/watcher v1.0.7
github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5
github.com/sebdah/goldie/v2 v2.5.5
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.9.0
golang.org/x/term v0.26.0
golang.org/x/sync v0.13.0
golang.org/x/term v0.31.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.10.0
mvdan.cc/sh/v3 v3.11.0
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

190
go.sum
View File

@@ -5,63 +5,79 @@ github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6h
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -84,117 +100,89 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -205,5 +193,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=

22
help.go
View File

@@ -41,20 +41,6 @@ func (o ListOptions) ShouldListTasks() bool {
return o.ListOnlyTasksWithDescriptions || o.ListAllTasks
}
// Validate validates that the collection of list-related options are in a valid configuration
func (o ListOptions) Validate() error {
if o.ListOnlyTasksWithDescriptions && o.ListAllTasks {
return fmt.Errorf("task: cannot use --list and --list-all at the same time")
}
if o.FormatTaskListAsJSON && !o.ShouldListTasks() {
return fmt.Errorf("task: --json only applies to --list or --list-all")
}
if o.NoStatus && !o.FormatTaskListAsJSON {
return fmt.Errorf("task: --no-status only applies to --json with --list or --list-all")
}
return nil
}
// Filters returns the slice of FilterFunc which filters a list
// of ast.Task according to the given ListOptions
func (o ListOptions) Filters() []FilterFunc {
@@ -128,18 +114,14 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
w = e.Stdout
}
// Get the list of tasks and sort them
tasks := e.Taskfile.Tasks.Values()
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
}
e.TaskSorter.Sort(tasks)
// Create a list of task names
taskNames := make([]string, 0, e.Taskfile.Tasks.Len())
for _, task := range tasks {
for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
if (allTasks || task.Desc != "") && !task.Internal {
taskNames = append(taskNames, strings.TrimRight(task.Task, ":"))
for _, alias := range task.Aliases {

52
init.go
View File

@@ -1,41 +1,41 @@
package task
import (
"fmt"
"io"
_ "embed"
"os"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
)
const defaultTaskfile = `# https://taskfile.dev
const defaultTaskFilename = "Taskfile.yml"
version: '3'
//go:embed taskfile/templates/default.yml
var DefaultTaskfile string
vars:
GREETING: Hello, World!
tasks:
default:
cmds:
- echo "{{.GREETING}}"
silent: true
`
const defaultTaskfileName = "Taskfile.yml"
// InitTaskfile Taskfile creates a new Taskfile
func InitTaskfile(w io.Writer, dir string) error {
f := filepathext.SmartJoin(dir, defaultTaskfileName)
if _, err := os.Stat(f); err == nil {
return errors.TaskfileAlreadyExistsError{}
// InitTaskfile creates a new Taskfile at path.
//
// path can be either a file path or a directory path.
// If path is a directory, path/Taskfile.yml will be created.
//
// The final file path is always returned and may be different from the input path.
func InitTaskfile(path string) (string, error) {
fi, err := os.Stat(path)
if err == nil && !fi.IsDir() {
return path, errors.TaskfileAlreadyExistsError{}
}
if err := os.WriteFile(f, []byte(defaultTaskfile), 0o644); err != nil {
return err
if fi != nil && fi.IsDir() {
path = filepathext.SmartJoin(path, defaultTaskFilename)
// path was a directory, so check if Taskfile.yml exists in it
if _, err := os.Stat(path); err == nil {
return path, errors.TaskfileAlreadyExistsError{}
}
}
fmt.Fprintf(w, "%s created in the current directory\n", defaultTaskfile)
return nil
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
return path, err
}
return path, nil
}

52
init_test.go Normal file
View File

@@ -0,0 +1,52 @@
package task_test
import (
"os"
"testing"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/filepathext"
)
func TestInitDir(t *testing.T) {
t.Parallel()
const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Taskfile.yml")
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.yml should not exist")
}
if _, err := task.InitTaskfile(dir); err != nil {
t.Error(err)
}
if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exist")
}
_ = os.Remove(file)
}
func TestInitFile(t *testing.T) {
t.Parallel()
const dir = "testdata/init"
file := filepathext.SmartJoin(dir, "Tasks.yml")
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Tasks.yml should not exist")
}
if _, err := task.InitTaskfile(file); err != nil {
t.Error(err)
}
if _, err := os.Stat(file); err != nil {
t.Errorf("Tasks.yml should exist")
}
_ = os.Remove(file)
}

View File

@@ -1,20 +0,0 @@
package compiler
import (
"os"
"strings"
"github.com/go-task/task/v3/taskfile/ast"
)
// GetEnviron the all return all environment variables encapsulated on a
// ast.Vars
func GetEnviron() *ast.Vars {
m := &ast.Vars{}
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, ast.Var{Value: val})
}
return m
}

View File

@@ -2,6 +2,8 @@ package deepcopy
import (
"reflect"
"github.com/elliotchance/orderedmap/v3"
)
type Copier[T any] interface {
@@ -38,6 +40,21 @@ func Map[K comparable, V any](orig map[K]V) map[K]V {
return c
}
func OrderedMap[K comparable, V any](orig *orderedmap.OrderedMap[K, V]) *orderedmap.OrderedMap[K, V] {
if orig.Len() == 0 {
return orderedmap.NewOrderedMap[K, V]()
}
c := orderedmap.NewOrderedMap[K, V]()
for pair := orig.Front(); pair != nil; pair = pair.Next() {
if copyable, ok := any(pair.Value).(Copier[V]); ok {
c.Set(pair.Key, copyable.DeepCopy())
} else {
c.Set(pair.Key, pair.Value)
}
}
return c
}
// TraverseStringsFunc runs the given function on every string in the given
// value by traversing it recursively. If the given value is a string, the
// function will run on a copy of the string and return it. If the value is a

31
internal/env/env.go vendored
View File

@@ -3,21 +3,42 @@ package env
import (
"fmt"
"os"
"strings"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast"
)
const taskVarPrefix = "TASK_"
// GetEnviron the all return all environment variables encapsulated on a
// ast.Vars
func GetEnviron() *ast.Vars {
m := ast.NewVars()
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, ast.Var{Value: val})
}
return m
}
func Get(t *ast.Task) []string {
if t.Env == nil {
return nil
}
return GetFromVars(t.Env)
}
func GetFromVars(env *ast.Vars) []string {
environ := os.Environ()
for k, v := range t.Env.ToCacheMap() {
for k, v := range env.ToCacheMap() {
if !isTypeAllowed(v) {
continue
}
if !experiments.EnvPrecedence.Enabled {
if !experiments.EnvPrecedence.Enabled() {
if _, alreadySet := os.LookupEnv(k); alreadySet {
continue
}
@@ -36,3 +57,7 @@ func isTypeAllowed(v any) bool {
return false
}
}
func GetTaskEnv(key string) string {
return os.Getenv(taskVarPrefix + key)
}

View File

@@ -11,13 +11,15 @@ import (
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
"github.com/go-task/task/v3/errors"
)
// RunCommandOptions is the options for the RunCommand func
// ErrNilOptions is returned when a nil options is given
var ErrNilOptions = errors.New("execext: nil options given")
// RunCommandOptions is the options for the [RunCommand] func.
type RunCommandOptions struct {
Command string
Dir string
@@ -29,9 +31,6 @@ type RunCommandOptions struct {
Stderr io.Writer
}
// ErrNilOptions is returned when a nil options is given
var ErrNilOptions = errors.New("execext: nil options given")
// RunCommand runs a shell command
func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
if opts == nil {
@@ -91,22 +90,64 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return r.Run(ctx, p)
}
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
// if available.
func Expand(s string) (string, error) {
func escape(s string) string {
s = filepath.ToSlash(s)
s = strings.ReplaceAll(s, " ", `\ `)
s = strings.ReplaceAll(s, "&", `\&`)
s = strings.ReplaceAll(s, "(", `\(`)
s = strings.ReplaceAll(s, ")", `\)`)
fields, err := shell.Fields(s, nil)
return s
}
// ExpandLiteral is a wrapper around [expand.Literal]. It will escape the input
// string, expand any shell symbols (such as '~') and resolve any environment
// variables.
func ExpandLiteral(s string) (string, error) {
if s == "" {
return "", nil
}
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil {
return "", err
}
if len(fields) > 0 {
return fields[0], nil
if len(words) == 0 {
return "", nil
}
return "", nil
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
}
return expand.Literal(cfg, words[0])
}
// ExpandFields is a wrapper around [expand.Fields]. It will escape the input
// string, expand any shell symbols (such as '~') and resolve any environment
// variables. It also expands brace expressions ({a.b}) and globs (*/**) and
// returns the results as a list of strings.
func ExpandFields(s string) ([]string, error) {
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil {
return nil, err
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
}
return expand.Fields(cfg, words...)
}
func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {

View File

@@ -1,26 +0,0 @@
// This package is intended as a place to copy functions from the
// golang.org/x/exp package. Copying these functions allows us to rely on our
// own code instead of an external package that may change unpredictably in the
// future.
//
// It also prevents problems with transitive dependencies whereby a
// package that imports Task (and therefore our version of golang.org/x/exp)
// cannot import a different version of golang.org/x/exp.
//
// Finally, it serves as a place to track functions that may be able to be
// removed in the future if they are added to the standard library. This is also
// why this package is under the internal directory since these functions are
// not intended to be used outside of Task.
package exp
import "cmp"
// Keys is a copy of https://pkg.go.dev/golang.org/x/exp@v0.0.0-20240103183307-be819d1f06fc/maps#Keys.
// This is not yet included in the standard library. See https://github.com/golang/go/issues/61538.
func Keys[K cmp.Ordered, V any](m map[K]V) []K {
var keys []K
for key := range m {
keys = append(keys, key)
}
return keys
}

View File

@@ -1,112 +0,0 @@
package experiments
import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/Ladicle/tabwriter"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
"github.com/go-task/task/v3/internal/logger"
)
const envPrefix = "TASK_X_"
type Experiment struct {
Name string
Enabled bool
Value string
}
// A list of experiments.
var (
GentleForce Experiment
RemoteTaskfiles Experiment
AnyVariables Experiment
MapVariables Experiment
EnvPrecedence Experiment
)
func init() {
readDotEnv()
GentleForce = New("GENTLE_FORCE")
RemoteTaskfiles = New("REMOTE_TASKFILES")
AnyVariables = New("ANY_VARIABLES", "1", "2")
MapVariables = New("MAP_VARIABLES", "1", "2")
EnvPrecedence = New("ENV_PRECEDENCE")
}
func New(xName string, enabledValues ...string) Experiment {
if len(enabledValues) == 0 {
enabledValues = []string{"1"}
}
value := getEnv(xName)
return Experiment{
Name: xName,
Enabled: slices.Contains(enabledValues, value),
Value: value,
}
}
func (x Experiment) String() string {
if x.Enabled {
return fmt.Sprintf("on (%s)", x.Value)
}
return "off"
}
func getEnv(xName string) string {
envName := fmt.Sprintf("%s%s", envPrefix, xName)
return os.Getenv(envName)
}
func getEnvFilePath() string {
// Parse the CLI flags again to get the directory/taskfile being run
// We use a flagset here so that we can parse a subset of flags without exiting on error.
var dir, taskfile string
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
fs.Usage = func() {}
_ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory.
if dir != "" {
return filepath.Join(dir, ".env")
}
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
if taskfile != "" {
return filepath.Join(filepath.Dir(taskfile), ".env")
}
// Otherwise just use the current working directory.
return ".env"
}
func readDotEnv() {
env, _ := godotenv.Read(getEnvFilePath())
// If the env var is an experiment, set it.
for key, value := range env {
if strings.HasPrefix(key, envPrefix) {
os.Setenv(key, value)
}
}
}
func printExperiment(w io.Writer, l *logger.Logger, x Experiment) {
l.FOutf(w, logger.Yellow, "* ")
l.FOutf(w, logger.Green, x.Name)
l.FOutf(w, logger.Default, ": \t%s\n", x.String())
}
func List(l *logger.Logger) error {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0)
printExperiment(w, l, GentleForce)
printExperiment(w, l, RemoteTaskfiles)
printExperiment(w, l, MapVariables)
printExperiment(w, l, EnvPrecedence)
return w.Flush()
}

View File

@@ -55,3 +55,9 @@ func TryAbsToRel(abs string) string {
return rel
}
// IsExtOnly checks whether path points to a file with no name but with
// an extension, i.e. ".yaml"
func IsExtOnly(path string) bool {
return filepath.Base(path) == filepath.Ext(path)
}

View File

@@ -0,0 +1,320 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package fingerprint
import (
"context"
"github.com/go-task/task/v3/taskfile/ast"
mock "github.com/stretchr/testify/mock"
)
// NewMockStatusCheckable creates a new instance of MockStatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockStatusCheckable(t interface {
mock.TestingT
Cleanup(func())
}) *MockStatusCheckable {
mock := &MockStatusCheckable{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockStatusCheckable is an autogenerated mock type for the StatusCheckable type
type MockStatusCheckable struct {
mock.Mock
}
type MockStatusCheckable_Expecter struct {
mock *mock.Mock
}
func (_m *MockStatusCheckable) EXPECT() *MockStatusCheckable_Expecter {
return &MockStatusCheckable_Expecter{mock: &_m.Mock}
}
// IsUpToDate provides a mock function for the type MockStatusCheckable
func (_mock *MockStatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {
ret := _mock.Called(ctx, t)
if len(ret) == 0 {
panic("no return value specified for IsUpToDate")
}
var r0 bool
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok {
return returnFunc(ctx, t)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok {
r0 = returnFunc(ctx, t)
} else {
r0 = ret.Get(0).(bool)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok {
r1 = returnFunc(ctx, t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockStatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
type MockStatusCheckable_IsUpToDate_Call struct {
*mock.Call
}
// IsUpToDate is a helper method to define mock.On call
// - ctx
// - t
func (_e *MockStatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *MockStatusCheckable_IsUpToDate_Call {
return &MockStatusCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", ctx, t)}
}
func (_c *MockStatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *MockStatusCheckable_IsUpToDate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*ast.Task))
})
return _c
}
func (_c *MockStatusCheckable_IsUpToDate_Call) Return(b bool, err error) *MockStatusCheckable_IsUpToDate_Call {
_c.Call.Return(b, err)
return _c
}
func (_c *MockStatusCheckable_IsUpToDate_Call) RunAndReturn(run func(ctx context.Context, t *ast.Task) (bool, error)) *MockStatusCheckable_IsUpToDate_Call {
_c.Call.Return(run)
return _c
}
// NewMockSourcesCheckable creates a new instance of MockSourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockSourcesCheckable(t interface {
mock.TestingT
Cleanup(func())
}) *MockSourcesCheckable {
mock := &MockSourcesCheckable{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockSourcesCheckable is an autogenerated mock type for the SourcesCheckable type
type MockSourcesCheckable struct {
mock.Mock
}
type MockSourcesCheckable_Expecter struct {
mock *mock.Mock
}
func (_m *MockSourcesCheckable) EXPECT() *MockSourcesCheckable_Expecter {
return &MockSourcesCheckable_Expecter{mock: &_m.Mock}
}
// IsUpToDate provides a mock function for the type MockSourcesCheckable
func (_mock *MockSourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) {
ret := _mock.Called(t)
if len(ret) == 0 {
panic("no return value specified for IsUpToDate")
}
var r0 bool
var r1 error
if returnFunc, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok {
return returnFunc(t)
}
if returnFunc, ok := ret.Get(0).(func(*ast.Task) bool); ok {
r0 = returnFunc(t)
} else {
r0 = ret.Get(0).(bool)
}
if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {
r1 = returnFunc(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockSourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
type MockSourcesCheckable_IsUpToDate_Call struct {
*mock.Call
}
// IsUpToDate is a helper method to define mock.On call
// - t
func (_e *MockSourcesCheckable_Expecter) IsUpToDate(t interface{}) *MockSourcesCheckable_IsUpToDate_Call {
return &MockSourcesCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", t)}
}
func (_c *MockSourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_IsUpToDate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *MockSourcesCheckable_IsUpToDate_Call) Return(b bool, err error) *MockSourcesCheckable_IsUpToDate_Call {
_c.Call.Return(b, err)
return _c
}
func (_c *MockSourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(t *ast.Task) (bool, error)) *MockSourcesCheckable_IsUpToDate_Call {
_c.Call.Return(run)
return _c
}
// Kind provides a mock function for the type MockSourcesCheckable
func (_mock *MockSourcesCheckable) Kind() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Kind")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockSourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind'
type MockSourcesCheckable_Kind_Call struct {
*mock.Call
}
// Kind is a helper method to define mock.On call
func (_e *MockSourcesCheckable_Expecter) Kind() *MockSourcesCheckable_Kind_Call {
return &MockSourcesCheckable_Kind_Call{Call: _e.mock.On("Kind")}
}
func (_c *MockSourcesCheckable_Kind_Call) Run(run func()) *MockSourcesCheckable_Kind_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockSourcesCheckable_Kind_Call) Return(s string) *MockSourcesCheckable_Kind_Call {
_c.Call.Return(s)
return _c
}
func (_c *MockSourcesCheckable_Kind_Call) RunAndReturn(run func() string) *MockSourcesCheckable_Kind_Call {
_c.Call.Return(run)
return _c
}
// OnError provides a mock function for the type MockSourcesCheckable
func (_mock *MockSourcesCheckable) OnError(t *ast.Task) error {
ret := _mock.Called(t)
if len(ret) == 0 {
panic("no return value specified for OnError")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(*ast.Task) error); ok {
r0 = returnFunc(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockSourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError'
type MockSourcesCheckable_OnError_Call struct {
*mock.Call
}
// OnError is a helper method to define mock.On call
// - t
func (_e *MockSourcesCheckable_Expecter) OnError(t interface{}) *MockSourcesCheckable_OnError_Call {
return &MockSourcesCheckable_OnError_Call{Call: _e.mock.On("OnError", t)}
}
func (_c *MockSourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_OnError_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *MockSourcesCheckable_OnError_Call) Return(err error) *MockSourcesCheckable_OnError_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockSourcesCheckable_OnError_Call) RunAndReturn(run func(t *ast.Task) error) *MockSourcesCheckable_OnError_Call {
_c.Call.Return(run)
return _c
}
// Value provides a mock function for the type MockSourcesCheckable
func (_mock *MockSourcesCheckable) Value(t *ast.Task) (any, error) {
ret := _mock.Called(t)
if len(ret) == 0 {
panic("no return value specified for Value")
}
var r0 any
var r1 error
if returnFunc, ok := ret.Get(0).(func(*ast.Task) (any, error)); ok {
return returnFunc(t)
}
if returnFunc, ok := ret.Get(0).(func(*ast.Task) any); ok {
r0 = returnFunc(t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(any)
}
}
if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {
r1 = returnFunc(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockSourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'
type MockSourcesCheckable_Value_Call struct {
*mock.Call
}
// Value is a helper method to define mock.On call
// - t
func (_e *MockSourcesCheckable_Expecter) Value(t interface{}) *MockSourcesCheckable_Value_Call {
return &MockSourcesCheckable_Value_Call{Call: _e.mock.On("Value", t)}
}
func (_c *MockSourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_Value_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *MockSourcesCheckable_Value_Call) Return(v any, err error) *MockSourcesCheckable_Value_Call {
_c.Call.Return(v, err)
return _c
}
func (_c *MockSourcesCheckable_Value_Call) RunAndReturn(run func(t *ast.Task) (any, error)) *MockSourcesCheckable_Value_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -4,47 +4,34 @@ import (
"os"
"sort"
"github.com/mattn/go-zglob"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
func Globs(dir string, globs []*ast.Glob) ([]string, error) {
fileMap := make(map[string]bool)
resultMap := make(map[string]bool)
for _, g := range globs {
matches, err := Glob(dir, g.Glob)
matches, err := glob(dir, g.Glob)
if err != nil {
continue
}
for _, match := range matches {
fileMap[match] = !g.Negate
resultMap[match] = !g.Negate
}
}
files := make([]string, 0)
for file, includePath := range fileMap {
if includePath {
files = append(files, file)
}
}
sort.Strings(files)
return files, nil
return collectKeys(resultMap), nil
}
func Glob(dir string, g string) ([]string, error) {
files := make([]string, 0)
func glob(dir string, g string) ([]string, error) {
g = filepathext.SmartJoin(dir, g)
g, err := execext.Expand(g)
fs, err := execext.ExpandFields(g)
if err != nil {
return nil, err
}
fs, err := zglob.GlobFollowSymlinks(g)
if err != nil {
return nil, err
}
results := make(map[string]bool, len(fs))
for _, f := range fs {
info, err := os.Stat(f)
@@ -54,7 +41,18 @@ func Glob(dir string, g string) ([]string, error) {
if info.IsDir() {
continue
}
files = append(files, f)
results[f] = true
}
return files, nil
return collectKeys(results), nil
}
func collectKeys(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k, v := range m {
if v {
keys = append(keys, k)
}
}
sort.Strings(keys)
return keys
}

View File

@@ -56,7 +56,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
if g.Negate {
continue
}
generates, err := Glob(t.Dir, g.Glob)
generates, err := glob(t.Dir, g.Glob)
if os.IsNotExist(err) {
return false, nil
}

View File

@@ -7,6 +7,8 @@ import (
)
func TestNormalizeFilename(t *testing.T) {
t.Parallel()
tests := []struct {
In, Out string
}{

View File

@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/mocks"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -26,11 +25,13 @@ import (
// | false | true | false |
// | false | false | false |
func TestIsTaskUpToDate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
task *ast.Task
setupMockStatusChecker func(m *mocks.StatusCheckable)
setupMockSourcesChecker func(m *mocks.SourcesCheckable)
setupMockStatusChecker func(m *MockStatusCheckable)
setupMockSourcesChecker func(m *MockSourcesCheckable)
expected bool
}{
{
@@ -50,7 +51,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: nil,
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
},
expected: true,
@@ -62,7 +63,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: nil,
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
},
expected: false,
@@ -73,7 +74,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: nil,
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
},
setupMockSourcesChecker: nil,
@@ -85,10 +86,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
},
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
},
expected: true,
@@ -99,10 +100,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
},
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
},
expected: false,
@@ -113,7 +114,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: nil,
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
},
setupMockSourcesChecker: nil,
@@ -125,10 +126,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
},
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
},
expected: false,
@@ -139,10 +140,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}},
},
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
setupMockStatusChecker: func(m *MockStatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
},
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
},
expected: false,
@@ -150,12 +151,14 @@ func TestIsTaskUpToDate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStatusChecker := mocks.NewStatusCheckable(t)
t.Parallel()
mockStatusChecker := NewMockStatusCheckable(t)
if tt.setupMockStatusChecker != nil {
tt.setupMockStatusChecker(mockStatusChecker)
}
mockSourcesChecker := mocks.NewSourcesCheckable(t)
mockSourcesChecker := NewMockSourcesCheckable(t)
if tt.setupMockSourcesChecker != nil {
tt.setupMockSourcesChecker(mockSourcesChecker)
}

View File

@@ -4,13 +4,17 @@ import (
"cmp"
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -37,49 +41,70 @@ Options:
`
var (
Version bool
Help bool
Init bool
Completion string
List bool
ListAll bool
ListJson bool
TaskSort string
Status bool
NoStatus bool
Insecure bool
Force bool
ForceAll bool
Watch bool
Verbose bool
Silent bool
AssumeYes bool
Dry bool
Summary bool
ExitCode bool
Parallel bool
Concurrency int
Dir string
Entrypoint string
Output ast.Output
Color bool
Interval time.Duration
Global bool
Experiments bool
Download bool
Offline bool
ClearCache bool
Timeout time.Duration
Version bool
Help bool
Init bool
Completion string
List bool
ListAll bool
ListJson bool
TaskSort string
Status bool
NoStatus bool
Insecure bool
Force bool
ForceAll bool
Watch bool
Verbose bool
Silent bool
AssumeYes bool
Dry bool
Summary bool
ExitCode bool
Parallel bool
Concurrency int
Dir string
Entrypoint string
Output ast.Output
Color bool
Interval time.Duration
Global bool
Experiments bool
Download bool
Offline bool
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
)
func init() {
// Config files can enable experiments which alter the availability and/or
// behavior of some flags, so we need to parse the experiments before the
// flags. However, we need the --taskfile and --dir flags before we can
// parse the experiments as they can alter the location of the config files.
// Because of this circular dependency, we parse the flags twice. First, we
// get the --taskfile and --dir flags, then we parse the experiments, then
// we parse the flags again to get the full set. We use a flagset here so
// that we can parse a subset of flags without exiting on error.
var dir, entrypoint string
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
fs.StringVarP(&dir, "dir", "d", "", "")
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
fs.Usage = func() {}
_ = fs.Parse(os.Args[1:])
// Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
// Parse the rest of the flags
log.SetFlags(0)
log.SetOutput(os.Stderr)
pflag.Usage = func() {
log.Print(usage)
pflag.PrintDefaults()
}
offline, err := strconv.ParseBool(cmp.Or(os.Getenv("TASK_OFFLINE"), "false"))
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil {
offline = false
}
@@ -102,7 +127,7 @@ func init() {
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
pflag.StringVarP(&Dir, "dir", "d", "", "Sets directory of execution.")
pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.")
pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
@@ -115,7 +140,7 @@ func init() {
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
// Gentle force experiment will override the force flag and add a new force-all flag
if experiments.GentleForce.Enabled {
if experiments.GentleForce.Enabled() {
pflag.BoolVarP(&Force, "force", "f", false, "Forces execution of the directly called task.")
pflag.BoolVar(&ForceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.")
} else {
@@ -123,11 +148,12 @@ func init() {
}
// Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled {
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.")
}
pflag.Parse()
@@ -143,8 +169,7 @@ func Validate() error {
}
if Global && Dir != "" {
log.Fatal("task: You can't set both --global and --dir")
return nil
return errors.New("task: You can't set both --global and --dir")
}
if Output.Name != "group" {
@@ -159,5 +184,70 @@ func Validate() error {
}
}
if List && ListAll {
return errors.New("task: cannot use --list and --list-all at the same time")
}
if ListJson && !List && !ListAll {
return errors.New("task: --json only applies to --list or --list-all")
}
if NoStatus && !ListJson {
return errors.New("task: --no-status only applies to --json with --list or --list-all")
}
return nil
}
// WithFlags is a special internal functional option that is used to pass flags
// from the CLI into any constructor that accepts functional options.
func WithFlags() task.ExecutorOption {
return &flagsOption{}
}
type flagsOption struct{}
func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
// Set the sorter
var sorter sort.Sorter
switch TaskSort {
case "none":
sorter = sort.NoSort
case "alphanumeric":
sorter = sort.AlphaNumeric
}
// Change the directory to the user's home directory if the global flag is set
dir := Dir
if Global {
home, err := os.UserHomeDir()
if err == nil {
dir = home
}
}
e.Options(
task.WithDir(dir),
task.WithEntrypoint(Entrypoint),
task.WithForce(Force),
task.WithForceAll(ForceAll),
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
task.WithAssumeYes(AssumeYes),
task.WithDry(Dry || Status),
task.WithSummary(Summary),
task.WithParallel(Parallel),
task.WithColor(Color),
task.WithConcurrency(Concurrency),
task.WithInterval(Interval),
task.WithOutputStyle(Output),
task.WithTaskSorter(sorter),
task.WithVersionCheck(true),
)
}

146
internal/fsext/fs.go Normal file
View File

@@ -0,0 +1,146 @@
package fsext
import (
"os"
"path/filepath"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/sysinfo"
)
// DefaultDir will return the default directory given an entrypoint or
// directory. If the directory is set, it will ensure it is an absolute path and
// return it. If the entrypoint is set, but the directory is not, it will leave
// the directory blank. If both are empty, it will default the directory to the
// current working directory.
func DefaultDir(entrypoint, dir string) string {
// If the directory is set, ensure it is an absolute path
if dir != "" {
var err error
dir, err = filepath.Abs(dir)
if err != nil {
return ""
}
return dir
}
// If the entrypoint and dir are empty, we default the directory to the current working directory
if entrypoint == "" {
wd, err := os.Getwd()
if err != nil {
return ""
}
return wd
}
// If the entrypoint is set, but the directory is not, we leave the directory blank
return ""
}
// Search will look for files with the given possible filenames using the given
// entrypoint and directory. If the entrypoint is set, it will check if the
// entrypoint matches a file or if it matches a directory containing one of the
// possible filenames. Otherwise, it will walk up the file tree starting at the
// given directory and perform a search in each directory for the possible
// filenames until it finds a match or reaches the root directory. If the
// entrypoint and directory are both empty, it will default the directory to the
// current working directory and perform a recursive search starting there. If a
// match is found, the absolute path to the file will be returned with its
// directory. If no match is found, an error will be returned.
func Search(entrypoint, dir string, possibleFilenames []string) (string, string, error) {
var err error
if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil {
return "", "", err
}
if dir == "" {
dir = filepath.Dir(entrypoint)
} else {
dir, err = filepath.Abs(dir)
if err != nil {
return "", "", err
}
}
return entrypoint, dir, nil
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return "", "", err
}
}
entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
if err != nil {
return "", "", err
}
dir = filepath.Dir(entrypoint)
return entrypoint, dir, nil
}
// Search will check if a file at the given path exists or not. If it does, it
// will return the path to it. If it does not, it will search for any files at
// the given path with any of the given possible 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 SearchPath(path string, possibleFilenames []string) (string, error) {
// Get file info about the path
fi, err := os.Stat(path)
if err != nil {
return "", err
}
// If the path exists and is a regular file, device, symlink, or named pipe,
// return the absolute path to it
if fi.Mode().IsRegular() ||
fi.Mode()&os.ModeDevice != 0 ||
fi.Mode()&os.ModeSymlink != 0 ||
fi.Mode()&os.ModeNamedPipe != 0 {
return filepath.Abs(path)
}
// If the path is a directory, check if any of the possible names exist
// in that directory
for _, filename := range possibleFilenames {
alt := filepathext.SmartJoin(path, filename)
if _, err := os.Stat(alt); err == nil {
return filepath.Abs(alt)
}
}
return "", os.ErrNotExist
}
// SearchRecursively 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 Search 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 SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
owner, err := sysinfo.Owner(path)
if err != nil {
return "", err
}
for {
fpath, err := SearchPath(path, possibleFilenames)
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 "", os.ErrNotExist
}
owner = parentOwner
path = parentPath
}
}

152
internal/fsext/fs_test.go Normal file
View File

@@ -0,0 +1,152 @@
package fsext
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestDefaultDir(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
entrypoint string
dir string
expected string
}{
{
name: "default to current working directory",
entrypoint: "",
dir: "",
expected: wd,
},
{
name: "resolves relative dir path",
entrypoint: "",
dir: "./dir",
expected: filepath.Join(wd, "dir"),
},
{
name: "return entrypoint if set",
entrypoint: filepath.Join(wd, "entrypoint"),
dir: "",
expected: "",
},
{
name: "if entrypoint and dir are set",
entrypoint: filepath.Join(wd, "entrypoint"),
dir: filepath.Join(wd, "dir"),
expected: filepath.Join(wd, "dir"),
},
{
name: "if entrypoint and dir are set and dir is relative",
entrypoint: filepath.Join(wd, "entrypoint"),
dir: "./dir",
expected: filepath.Join(wd, "dir"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, DefaultDir(tt.entrypoint, tt.dir))
})
}
}
func TestSearch(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
entrypoint string
dir string
possibleFilenames []string
expectedEntrypoint string
expectedDir string
}{
{
name: "find foo.txt using relative entrypoint",
entrypoint: "./testdata/foo.txt",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute entrypoint",
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir",
dir: "./testdata",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute dir",
dir: filepath.Join(wd, "testdata"),
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir and relative entrypoint",
entrypoint: "./testdata/foo.txt",
dir: "./testdata/some/other/dir",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
},
{
name: "find fs.go using no entrypoint or dir",
entrypoint: "",
dir: "",
possibleFilenames: []string{"fs.go"},
expectedEntrypoint: filepath.Join(wd, "fs.go"),
expectedDir: wd,
},
{
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
entrypoint: "",
dir: "",
possibleFilenames: []string{"Taskfile.yml"},
expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
expectedDir: filepath.Join(wd, "..", ".."),
},
{
name: "find foo.txt first if listed first in possible filenames",
entrypoint: "./testdata",
possibleFilenames: []string{"foo.txt", "bar.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find bar.txt first if listed first in possible filenames",
entrypoint: "./testdata",
possibleFilenames: []string{"bar.txt", "foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
entrypoint, dir, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
require.NoError(t, err)
require.Equal(t, tt.expectedEntrypoint, entrypoint)
require.Equal(t, tt.expectedDir, dir)
})
}
}

0
internal/fsext/testdata/bar.txt vendored Normal file
View File

0
internal/fsext/testdata/foo.txt vendored Normal file
View File

View File

@@ -0,0 +1,56 @@
package fsnotifyext
import (
"math"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
type Deduper struct {
w *fsnotify.Watcher
waitTime time.Duration
mutex sync.Mutex
}
func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
return &Deduper{
w: w,
waitTime: waitTime,
}
}
func (d *Deduper) GetChan() chan fsnotify.Event {
channel := make(chan fsnotify.Event)
timers := make(map[string]*time.Timer)
go func() {
for {
event, ok := <-d.w.Events
switch {
case !ok:
return
case event.Op == fsnotify.Chmod:
continue
}
d.mutex.Lock()
timer, ok := timers[event.String()]
d.mutex.Unlock()
if !ok {
timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
timer.Stop()
d.mutex.Lock()
timers[event.String()] = timer
d.mutex.Unlock()
}
timer.Reset(d.waitTime)
}
}()
return channel
}

View File

@@ -8,9 +8,12 @@ import (
"strconv"
"strings"
"github.com/Ladicle/tabwriter"
"github.com/fatih/color"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/term"
)
@@ -19,70 +22,86 @@ var (
ErrNoTerminal = errors.New("no terminal")
)
var (
attrsReset = envColor("COLOR_RESET", color.Reset)
attrsFgBlue = envColor("COLOR_BLUE", color.FgBlue)
attrsFgGreen = envColor("COLOR_GREEN", color.FgGreen)
attrsFgCyan = envColor("COLOR_CYAN", color.FgCyan)
attrsFgYellow = envColor("COLOR_YELLOW", color.FgYellow)
attrsFgMagenta = envColor("COLOR_MAGENTA", color.FgMagenta)
attrsFgRed = envColor("COLOR_RED", color.FgRed)
attrsFgHiBlue = envColor("COLOR_BRIGHT_BLUE", color.FgHiBlue)
attrsFgHiGreen = envColor("COLOR_BRIGHT_GREEN", color.FgHiGreen)
attrsFgHiCyan = envColor("COLOR_BRIGHT_CYAN", color.FgHiCyan)
attrsFgHiYellow = envColor("COLOR_BRIGHT_YELLOW", color.FgHiYellow)
attrsFgHiMagenta = envColor("COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)
attrsFgHiRed = envColor("COLOR_BRIGHT_RED", color.FgHiRed)
)
type (
Color func() PrintFunc
PrintFunc func(io.Writer, string, ...any)
)
func Default() PrintFunc {
return color.New(envColor("TASK_COLOR_RESET", color.Reset)...).FprintfFunc()
return color.New(attrsReset...).FprintfFunc()
}
func Blue() PrintFunc {
return color.New(envColor("TASK_COLOR_BLUE", color.FgBlue)...).FprintfFunc()
return color.New(attrsFgBlue...).FprintfFunc()
}
func Green() PrintFunc {
return color.New(envColor("TASK_COLOR_GREEN", color.FgGreen)...).FprintfFunc()
return color.New(attrsFgGreen...).FprintfFunc()
}
func Cyan() PrintFunc {
return color.New(envColor("TASK_COLOR_CYAN", color.FgCyan)...).FprintfFunc()
return color.New(attrsFgCyan...).FprintfFunc()
}
func Yellow() PrintFunc {
return color.New(envColor("TASK_COLOR_YELLOW", color.FgYellow)...).FprintfFunc()
return color.New(attrsFgYellow...).FprintfFunc()
}
func Magenta() PrintFunc {
return color.New(envColor("TASK_COLOR_MAGENTA", color.FgMagenta)...).FprintfFunc()
return color.New(attrsFgMagenta...).FprintfFunc()
}
func Red() PrintFunc {
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
return color.New(attrsFgRed...).FprintfFunc()
}
func BrightBlue() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
return color.New(attrsFgHiBlue...).FprintfFunc()
}
func BrightGreen() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
return color.New(attrsFgHiGreen...).FprintfFunc()
}
func BrightCyan() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
return color.New(attrsFgHiCyan...).FprintfFunc()
}
func BrightYellow() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
return color.New(attrsFgHiYellow...).FprintfFunc()
}
func BrightMagenta() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
return color.New(attrsFgHiMagenta...).FprintfFunc()
}
func BrightRed() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
return color.New(attrsFgHiRed...).FprintfFunc()
}
func envColor(env string, defaultColor color.Attribute) []color.Attribute {
func envColor(name string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false
}
// Fetch the environment variable
override := os.Getenv(env)
override := env.GetTaskEnv(name)
// First, try splitting the string by commas (RGB shortcut syntax) and if it
// matches, then prepend the 256-color foreground escape sequence.
@@ -195,3 +214,16 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
return nil
}
func (l *Logger) PrintExperiments() error {
w := tabwriter.NewWriter(l.Stdout, 0, 8, 0, ' ', 0)
for _, x := range experiments.List() {
if !x.Active() {
continue
}
l.FOutf(w, Yellow, "* ")
l.FOutf(w, Green, x.Name)
l.FOutf(w, Default, ": \t%s\n", x.String())
}
return w.Flush()
}

View File

@@ -1,226 +0,0 @@
// Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks
import (
ast "github.com/go-task/task/v3/taskfile/ast"
mock "github.com/stretchr/testify/mock"
)
// SourcesCheckable is an autogenerated mock type for the SourcesCheckable type
type SourcesCheckable struct {
mock.Mock
}
type SourcesCheckable_Expecter struct {
mock *mock.Mock
}
func (_m *SourcesCheckable) EXPECT() *SourcesCheckable_Expecter {
return &SourcesCheckable_Expecter{mock: &_m.Mock}
}
// IsUpToDate provides a mock function with given fields: t
func (_m *SourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) {
ret := _m.Called(t)
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok {
return rf(t)
}
if rf, ok := ret.Get(0).(func(*ast.Task) bool); ok {
r0 = rf(t)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(*ast.Task) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
type SourcesCheckable_IsUpToDate_Call struct {
*mock.Call
}
// IsUpToDate is a helper method to define mock.On call
// - t *ast.Task
func (_e *SourcesCheckable_Expecter) IsUpToDate(t interface{}) *SourcesCheckable_IsUpToDate_Call {
return &SourcesCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", t)}
}
func (_c *SourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *SourcesCheckable_IsUpToDate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *SourcesCheckable_IsUpToDate_Call) Return(_a0 bool, _a1 error) *SourcesCheckable_IsUpToDate_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(*ast.Task) (bool, error)) *SourcesCheckable_IsUpToDate_Call {
_c.Call.Return(run)
return _c
}
// Kind provides a mock function with given fields:
func (_m *SourcesCheckable) Kind() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// SourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind'
type SourcesCheckable_Kind_Call struct {
*mock.Call
}
// Kind is a helper method to define mock.On call
func (_e *SourcesCheckable_Expecter) Kind() *SourcesCheckable_Kind_Call {
return &SourcesCheckable_Kind_Call{Call: _e.mock.On("Kind")}
}
func (_c *SourcesCheckable_Kind_Call) Run(run func()) *SourcesCheckable_Kind_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *SourcesCheckable_Kind_Call) Return(_a0 string) *SourcesCheckable_Kind_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SourcesCheckable_Kind_Call) RunAndReturn(run func() string) *SourcesCheckable_Kind_Call {
_c.Call.Return(run)
return _c
}
// OnError provides a mock function with given fields: t
func (_m *SourcesCheckable) OnError(t *ast.Task) error {
ret := _m.Called(t)
var r0 error
if rf, ok := ret.Get(0).(func(*ast.Task) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// SourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError'
type SourcesCheckable_OnError_Call struct {
*mock.Call
}
// OnError is a helper method to define mock.On call
// - t *ast.Task
func (_e *SourcesCheckable_Expecter) OnError(t interface{}) *SourcesCheckable_OnError_Call {
return &SourcesCheckable_OnError_Call{Call: _e.mock.On("OnError", t)}
}
func (_c *SourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *SourcesCheckable_OnError_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *SourcesCheckable_OnError_Call) Return(_a0 error) *SourcesCheckable_OnError_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SourcesCheckable_OnError_Call) RunAndReturn(run func(*ast.Task) error) *SourcesCheckable_OnError_Call {
_c.Call.Return(run)
return _c
}
// Value provides a mock function with given fields: t
func (_m *SourcesCheckable) Value(t *ast.Task) (interface{}, error) {
ret := _m.Called(t)
var r0 interface{}
var r1 error
if rf, ok := ret.Get(0).(func(*ast.Task) (interface{}, error)); ok {
return rf(t)
}
if rf, ok := ret.Get(0).(func(*ast.Task) interface{}); ok {
r0 = rf(t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(interface{})
}
}
if rf, ok := ret.Get(1).(func(*ast.Task) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'
type SourcesCheckable_Value_Call struct {
*mock.Call
}
// Value is a helper method to define mock.On call
// - t *ast.Task
func (_e *SourcesCheckable_Expecter) Value(t interface{}) *SourcesCheckable_Value_Call {
return &SourcesCheckable_Value_Call{Call: _e.mock.On("Value", t)}
}
func (_c *SourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *SourcesCheckable_Value_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*ast.Task))
})
return _c
}
func (_c *SourcesCheckable_Value_Call) Return(_a0 interface{}, _a1 error) *SourcesCheckable_Value_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SourcesCheckable_Value_Call) RunAndReturn(run func(*ast.Task) (interface{}, error)) *SourcesCheckable_Value_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewSourcesCheckable interface {
mock.TestingT
Cleanup(func())
}
// NewSourcesCheckable creates a new instance of SourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewSourcesCheckable(t mockConstructorTestingTNewSourcesCheckable) *SourcesCheckable {
mock := &SourcesCheckable{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,92 +0,0 @@
// Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks
import (
context "context"
ast "github.com/go-task/task/v3/taskfile/ast"
mock "github.com/stretchr/testify/mock"
)
// StatusCheckable is an autogenerated mock type for the StatusCheckable type
type StatusCheckable struct {
mock.Mock
}
type StatusCheckable_Expecter struct {
mock *mock.Mock
}
func (_m *StatusCheckable) EXPECT() *StatusCheckable_Expecter {
return &StatusCheckable_Expecter{mock: &_m.Mock}
}
// IsUpToDate provides a mock function with given fields: ctx, t
func (_m *StatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {
ret := _m.Called(ctx, t)
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok {
return rf(ctx, t)
}
if rf, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok {
r0 = rf(ctx, t)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok {
r1 = rf(ctx, t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// StatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
type StatusCheckable_IsUpToDate_Call struct {
*mock.Call
}
// IsUpToDate is a helper method to define mock.On call
// - ctx context.Context
// - t *ast.Task
func (_e *StatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *StatusCheckable_IsUpToDate_Call {
return &StatusCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", ctx, t)}
}
func (_c *StatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *StatusCheckable_IsUpToDate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*ast.Task))
})
return _c
}
func (_c *StatusCheckable_IsUpToDate_Call) Return(_a0 bool, _a1 error) *StatusCheckable_IsUpToDate_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *StatusCheckable_IsUpToDate_Call) RunAndReturn(run func(context.Context, *ast.Task) (bool, error)) *StatusCheckable_IsUpToDate_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewStatusCheckable interface {
mock.TestingT
Cleanup(func())
}
// NewStatusCheckable creates a new instance of StatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewStatusCheckable(t mockConstructorTestingTNewStatusCheckable) *StatusCheckable {
mock := &StatusCheckable{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,164 +0,0 @@
package omap
import (
"cmp"
"fmt"
"slices"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/exp"
)
// An OrderedMap is a wrapper around a regular map that maintains an ordered
// list of the map's keys. This allows you to run deterministic and ordered
// operations on the map such as printing/serializing/iterating.
type OrderedMap[K cmp.Ordered, V any] struct {
s []K
m map[K]V
}
// New will create a new OrderedMap of the given type and return it.
func New[K cmp.Ordered, V any]() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: make([]K, 0),
m: make(map[K]V),
}
}
// FromMap will create a new OrderedMap from the given map. Since Golang maps
// are unordered, the order of the created OrderedMap will be random.
func FromMap[K cmp.Ordered, V any](m map[K]V) OrderedMap[K, V] {
om := New[K, V]()
om.m = m
om.s = exp.Keys(m)
return om
}
func FromMapWithOrder[K cmp.Ordered, V any](m map[K]V, order []K) OrderedMap[K, V] {
om := New[K, V]()
if len(m) != len(order) {
panic("length of map and order must be equal")
}
om.m = m
om.s = order
for key := range om.m {
if !slices.Contains(om.s, key) {
panic("order keys must match map keys")
}
}
return om
}
// Len will return the number of items in the map.
func (om *OrderedMap[K, V]) Len() int {
return len(om.s)
}
// Set will set the value for a given key.
func (om *OrderedMap[K, V]) Set(key K, value V) {
if om.m == nil {
om.m = make(map[K]V)
}
if _, ok := om.m[key]; !ok {
om.s = append(om.s, key)
}
om.m[key] = value
}
// Get will return the value for a given key.
// If the key does not exist, it will return the zero value of the value type.
func (om *OrderedMap[K, V]) Get(key K) V {
value, ok := om.m[key]
if !ok {
var zero V
return zero
}
return value
}
// Exists will return whether or not the given key exists.
func (om *OrderedMap[K, V]) Exists(key K) bool {
_, ok := om.m[key]
return ok
}
// Sort will sort the map.
func (om *OrderedMap[K, V]) Sort() {
slices.Sort(om.s)
}
// SortFunc will sort the map using the given function.
func (om *OrderedMap[K, V]) SortFunc(less func(i, j K) int) {
slices.SortFunc(om.s, less)
}
// Keys will return a slice of the map's keys in order.
func (om *OrderedMap[K, V]) Keys() []K {
return om.s
}
// Values will return a slice of the map's values in order.
func (om *OrderedMap[K, V]) Values() []V {
var values []V
for _, key := range om.s {
values = append(values, om.m[key])
}
return values
}
// Range will iterate over the map and call the given function for each key/value.
func (om *OrderedMap[K, V]) Range(fn func(key K, value V) error) error {
for _, key := range om.s {
if err := fn(key, om.m[key]); err != nil {
return err
}
}
return nil
}
// Merge merges the given Vars into the caller one
func (om *OrderedMap[K, V]) Merge(other OrderedMap[K, V]) {
// nolint: errcheck
other.Range(func(key K, value V) error {
om.Set(key, value)
return nil
})
}
func (om *OrderedMap[K, V]) DeepCopy() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: deepcopy.Slice(om.s),
m: deepcopy.Map(om.m),
}
}
func (om *OrderedMap[K, V]) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
// Even numbers contain the keys
// Odd numbers contain the values
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
// Decode the key
keyNode := node.Content[i]
var k K
if err := keyNode.Decode(&k); err != nil {
return err
}
// Decode the value
valueNode := node.Content[i+1]
var v V
if err := valueNode.Decode(&v); err != nil {
return err
}
// Set the key and value
om.Set(k, v)
}
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
}

View File

@@ -1,121 +0,0 @@
package omap
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestFromMap(t *testing.T) {
m := map[int]string{3: "three", 1: "one", 2: "two"}
om := FromMap(m)
assert.Len(t, om.m, 3)
assert.Len(t, om.s, 3)
assert.ElementsMatch(t, []int{1, 2, 3}, om.s)
for key, value := range m {
assert.Equal(t, om.Get(key), value)
}
}
func TestSetGetExists(t *testing.T) {
om := New[int, string]()
assert.False(t, om.Exists(1))
assert.Equal(t, "", om.Get(1))
om.Set(1, "one")
assert.True(t, om.Exists(1))
assert.Equal(t, "one", om.Get(1))
}
func TestSort(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.Sort()
assert.Equal(t, []int{1, 2, 3}, om.s)
}
func TestSortFunc(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.SortFunc(func(a, b int) int {
return b - a
})
assert.Equal(t, []int{3, 2, 1}, om.s)
}
func TestKeysValues(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
assert.Equal(t, []int{3, 1, 2}, om.Keys())
assert.Equal(t, []string{"three", "one", "two"}, om.Values())
}
func Range(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
keys := make([]int, 0, len(expectedKeys))
values := make([]string, 0, len(expectedValues))
err := om.Range(func(key int, value string) error {
keys = append(keys, key)
values = append(values, value)
return nil
})
assert.NoError(t, err)
assert.ElementsMatch(t, expectedKeys, keys)
assert.ElementsMatch(t, expectedValues, values)
}
func TestOrderedMapMerge(t *testing.T) {
om1 := New[string, int]()
om1.Set("a", 1)
om1.Set("b", 2)
om2 := New[string, int]()
om2.Set("b", 3)
om2.Set("c", 4)
om1.Merge(om2)
expectedKeys := []string{"a", "b", "c"}
expectedValues := []int{1, 3, 4}
assert.Equal(t, len(expectedKeys), len(om1.s))
assert.Equal(t, len(expectedKeys), len(om1.m))
for i, key := range expectedKeys {
assert.True(t, om1.Exists(key))
assert.Equal(t, expectedValues[i], om1.Get(key))
}
}
func TestUnmarshalYAML(t *testing.T) {
yamlString := `
3: three
1: one
2: two
`
var om OrderedMap[int, string]
err := yaml.Unmarshal([]byte(yamlString), &om)
require.NoError(t, err)
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
assert.Equal(t, expectedKeys, om.Keys())
assert.Equal(t, expectedValues, om.Values())
}

View File

@@ -12,13 +12,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/logger"
"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"
)
func TestInterleaved(t *testing.T) {
t.Parallel()
var b bytes.Buffer
var o output.Output = output.Interleaved{}
w, _, _ := o.WrapWriter(&b, io.Discard, "", nil)
@@ -30,6 +31,8 @@ func TestInterleaved(t *testing.T) {
}
func TestGroup(t *testing.T) {
t.Parallel()
var b bytes.Buffer
var o output.Output = output.Group{}
stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil)
@@ -48,12 +51,15 @@ func TestGroup(t *testing.T) {
}
func TestGroupWithBeginEnd(t *testing.T) {
t.Parallel()
tmpl := templater.Cache{
Vars: &ast.Vars{
OrderedMap: omap.FromMap(map[string]ast.Var{
"VAR1": {Value: "example-value"},
}),
},
Vars: ast.NewVars(
&ast.VarElement{
Key: "VAR1",
Value: ast.Var{Value: "example-value"},
},
),
}
var o output.Output = output.Group{
@@ -61,6 +67,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
End: "::endgroup::",
}
t.Run("simple", func(t *testing.T) {
t.Parallel()
var b bytes.Buffer
w, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl)
@@ -72,6 +80,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
})
t.Run("no output", func(t *testing.T) {
t.Parallel()
var b bytes.Buffer
_, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl)
require.NoError(t, cleanup(nil))
@@ -80,6 +90,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
}
func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {
t.Parallel()
var b bytes.Buffer
var o output.Output = output.Group{
ErrorOnly: true,
@@ -94,6 +106,8 @@ func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {
}
func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
t.Parallel()
var b bytes.Buffer
var o output.Output = output.Group{
ErrorOnly: true,
@@ -107,7 +121,7 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
assert.Equal(t, "std-out\nstd-err\n", b.String())
}
func TestPrefixed(t *testing.T) {
func TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel
var b bytes.Buffer
l := &logger.Logger{
Color: false,
@@ -116,7 +130,7 @@ func TestPrefixed(t *testing.T) {
var o output.Output = output.NewPrefixed(l)
w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil)
t.Run("simple use cases", func(t *testing.T) {
t.Run("simple use cases", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel
b.Reset()
fmt.Fprintln(w, "foo\nbar")
@@ -126,7 +140,7 @@ func TestPrefixed(t *testing.T) {
require.NoError(t, cleanup(nil))
})
t.Run("multiple writes for a single line", func(t *testing.T) {
t.Run("multiple writes for a single line", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel
b.Reset()
for _, char := range []string{"T", "e", "s", "t", "!"} {
@@ -140,6 +154,8 @@ func TestPrefixed(t *testing.T) {
}
func TestPrefixedWithColor(t *testing.T) {
t.Parallel()
color.NoColor = false
var b bytes.Buffer
@@ -155,6 +171,8 @@ func TestPrefixedWithColor(t *testing.T) {
}
t.Run("colors should loop", func(t *testing.T) {
t.Parallel()
for i, w := range writers {
b.Reset()
@@ -164,7 +182,11 @@ func TestPrefixedWithColor(t *testing.T) {
l.FOutf(&prefix, color, fmt.Sprintf("prefix-%d", i))
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()), b.String())
assert.Equal(
t,
fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()),
b.String(),
)
}
})
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"strings"
"sync"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
@@ -14,20 +15,21 @@ type Prefixed struct {
logger *logger.Logger
seen map[string]uint
counter *uint
mutex sync.Mutex
}
func NewPrefixed(logger *logger.Logger) Prefixed {
func NewPrefixed(logger *logger.Logger) *Prefixed {
var counter uint
return Prefixed{
return &Prefixed{
seen: make(map[string]uint),
counter: &counter,
logger: logger,
}
}
func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: &p}
func (p *Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: p}
return pw, pw, func(error) error { return pw.close() }
}
@@ -85,6 +87,9 @@ func (pw *prefixWriter) writeLine(line string) error {
line += "\n"
}
defer pw.prefixed.mutex.Unlock()
pw.prefixed.mutex.Lock()
idx, ok := pw.prefixed.seen[pw.prefix]
if !ok {

View File

@@ -18,3 +18,15 @@ func UniqueJoin[T cmp.Ordered](ss ...[]T) []T {
slices.Sort(r)
return slices.Compact(r)
}
func Convert[T, U any](s []T, f func(T) U) []U {
// Create a new slice with the same length as the input slice
result := make([]U, len(s))
// Convert each element using the provided function
for i, v := range s {
result[i] = f(v)
}
return result
}

View File

@@ -0,0 +1,86 @@
package slicesext
import (
"math"
"strconv"
"testing"
)
func TestConvertIntToString(t *testing.T) {
t.Parallel()
input := []int{1, 2, 3, 4, 5}
expected := []string{"1", "2", "3", "4", "5"}
result := Convert(input, strconv.Itoa)
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertStringToInt(t *testing.T) {
t.Parallel()
input := []string{"1", "2", "3", "4", "5"}
expected := []int{1, 2, 3, 4, 5}
result := Convert(input, func(s string) int {
n, _ := strconv.Atoi(s)
return n
})
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertFloatToInt(t *testing.T) {
t.Parallel()
input := []float64{1.1, 2.2, 3.7, 4.5, 5.9}
expected := []int{1, 2, 4, 5, 6}
result := Convert(input, func(f float64) int {
return int(math.Round(f))
})
if len(result) != len(expected) {
t.Errorf("Expected length %d, got %d", len(expected), len(result))
}
for i := range expected {
if result[i] != expected[i] {
t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i])
}
}
}
func TestConvertEmptySlice(t *testing.T) {
t.Parallel()
input := []int{}
result := Convert(input, strconv.Itoa)
if len(result) != 0 {
t.Errorf("Expected empty slice, got length %d", len(result))
}
}
func TestConvertNilSlice(t *testing.T) {
t.Parallel()
var input []int
result := Convert(input, strconv.Itoa)
if result == nil {
t.Error("Expected non-nil empty slice, got nil")
}
if len(result) != 0 {
t.Errorf("Expected empty slice, got length %d", len(result))
}
}

View File

@@ -1,44 +1,44 @@
package sort
import (
"slices"
"sort"
"strings"
"github.com/go-task/task/v3/taskfile/ast"
)
type TaskSorter interface {
Sort([]*ast.Task)
// A Sorter is any function that sorts a set of tasks.
type Sorter func(items []string, namespaces []string) []string
// NoSort leaves the tasks in the order they are defined.
func NoSort(items []string, namespaces []string) []string {
return items
}
type Noop struct{}
func (s *Noop) Sort(tasks []*ast.Task) {}
type AlphaNumeric struct{}
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
func (s *AlphaNumeric) Sort(tasks []*ast.Task) {
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].Task < tasks[j].Task
})
// AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order
// by task name.
func AlphaNumeric(items []string, namespaces []string) []string {
slices.Sort(items)
return items
}
type AlphaNumericWithRootTasksFirst struct{}
// Tasks that are not namespaced should be listed before tasks that are.
// We detect this by searching for a ':' in the task name.
func (s *AlphaNumericWithRootTasksFirst) Sort(tasks []*ast.Task) {
sort.Slice(tasks, func(i, j int) bool {
iContainsColon := strings.Contains(tasks[i].Task, ":")
jContainsColon := strings.Contains(tasks[j].Task, ":")
// AlphaNumericWithRootTasksFirst sorts the JSON output so that tasks are in
// alpha numeric order by task name. It will also ensure that tasks that are not
// namespaced will be listed before tasks that are. We detect this by searching
// for a ':' in the task name.
func AlphaNumericWithRootTasksFirst(items []string, namespaces []string) []string {
if len(namespaces) > 0 {
return AlphaNumeric(items, namespaces)
}
sort.Slice(items, func(i, j int) bool {
iContainsColon := strings.Contains(items[i], ":")
jContainsColon := strings.Contains(items[j], ":")
if iContainsColon == jContainsColon {
return tasks[i].Task < tasks[j].Task
return items[i] < items[j]
}
if !iContainsColon && jContainsColon {
return true
}
return false
})
return items
}

View File

@@ -4,74 +4,110 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/taskfile/ast"
)
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
task1 := &ast.Task{Task: "task1"}
task2 := &ast.Task{Task: "task2"}
task3 := &ast.Task{Task: "ns1:task3"}
task4 := &ast.Task{Task: "ns2:task4"}
task5 := &ast.Task{Task: "task5"}
task6 := &ast.Task{Task: "ns3:task6"}
t.Parallel()
item1 := "a-item1"
item2 := "m-item2"
item3 := "ns1:item3"
item4 := "ns2:item4"
item5 := "z-item5"
item6 := "ns3:item6"
tests := []struct {
name string
tasks []*ast.Task
want []*ast.Task
items []string
want []string
}{
{
name: "no namespace tasks sorted alphabetically first",
tasks: []*ast.Task{task3, task2, task1},
want: []*ast.Task{task1, task2, task3},
name: "no namespace items sorted alphabetically first",
items: []string{item3, item2, item1},
want: []string{item1, item2, item3},
},
{
name: "namespace tasks sorted alphabetically after non-namespaced tasks",
tasks: []*ast.Task{task3, task4, task5},
want: []*ast.Task{task5, task3, task4},
name: "namespace items sorted alphabetically after non-namespaced items",
items: []string{item3, item4, item5},
want: []string{item5, item3, item4},
},
{
name: "all tasks sorted alphabetically with root tasks first",
tasks: []*ast.Task{task6, task5, task4, task3, task2, task1},
want: []*ast.Task{task1, task2, task5, task3, task4, task6},
name: "all items sorted alphabetically with root items first",
items: []string{item6, item5, item4, item3, item2, item1},
want: []string{item1, item2, item5, item3, item4, item6},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &AlphaNumericWithRootTasksFirst{}
s.Sort(tt.tasks)
assert.Equal(t, tt.want, tt.tasks)
t.Parallel()
AlphaNumericWithRootTasksFirst(tt.items, nil)
assert.Equal(t, tt.want, tt.items)
})
}
}
func TestAlphaNumeric_Sort(t *testing.T) {
task1 := &ast.Task{Task: "task1"}
task2 := &ast.Task{Task: "task2"}
task3 := &ast.Task{Task: "ns1:task3"}
task4 := &ast.Task{Task: "ns2:task4"}
task5 := &ast.Task{Task: "task5"}
task6 := &ast.Task{Task: "ns3:task6"}
t.Parallel()
item1 := "a-item1"
item2 := "m-item2"
item3 := "ns1:item3"
item4 := "ns2:item4"
item5 := "z-item5"
item6 := "ns3:item6"
tests := []struct {
name string
tasks []*ast.Task
want []*ast.Task
items []string
want []string
}{
{
name: "all tasks sorted alphabetically",
tasks: []*ast.Task{task3, task2, task5, task1, task4, task6},
want: []*ast.Task{task3, task4, task6, task1, task2, task5},
name: "all items sorted alphabetically",
items: []string{item3, item2, item5, item1, item4, item6},
want: []string{item1, item2, item3, item4, item6, item5},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &AlphaNumeric{}
s.Sort(tt.tasks)
assert.Equal(t, tt.tasks, tt.want)
t.Parallel()
AlphaNumeric(tt.items, nil)
assert.Equal(t, tt.want, tt.items)
})
}
}
func TestNoSort_Sort(t *testing.T) {
t.Parallel()
item1 := "a-item1"
item2 := "m-item2"
item3 := "ns1:item3"
item4 := "ns2:item4"
item5 := "z-item5"
item6 := "ns3:item6"
tests := []struct {
name string
items []string
want []string
}{
{
name: "all items in order of definition",
items: []string{item3, item2, item5, item1, item4, item6},
want: []string{item3, item2, item5, item1, item4, item6},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
NoSort(tt.items, nil)
assert.Equal(t, tt.want, tt.items)
})
}
}

View File

@@ -7,10 +7,12 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) {
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []string) {
for i, call := range c {
PrintSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks.Get(call.Task))
if task, ok := t.Tasks.Get(call); ok {
PrintTask(l, task)
}
}
}

View File

@@ -13,6 +13,8 @@ import (
)
func TestPrintsDependenciesIfPresent(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Deps: []*ast.Dep{
@@ -38,6 +40,8 @@ func createDummyLogger() (*bytes.Buffer, logger.Logger) {
}
func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Deps: []*ast.Dep{},
@@ -49,6 +53,8 @@ func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
}
func TestPrintTaskName(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Task: "my-task-name",
@@ -60,6 +66,8 @@ func TestPrintTaskName(t *testing.T) {
}
func TestPrintTaskCommandsIfPresent(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Cmds: []*ast.Cmd{
@@ -78,6 +86,8 @@ func TestPrintTaskCommandsIfPresent(t *testing.T) {
}
func TestDoesNotPrintCommandIfMissing(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Cmds: []*ast.Cmd{},
@@ -89,6 +99,8 @@ func TestDoesNotPrintCommandIfMissing(t *testing.T) {
}
func TestLayout(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
task := &ast.Task{
Task: "sample-task",
@@ -123,6 +135,8 @@ commands:
}
func TestPrintDescriptionAsFallback(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
taskWithoutSummary := &ast.Task{
Desc: "description",
@@ -150,20 +164,23 @@ func TestPrintDescriptionAsFallback(t *testing.T) {
}
func TestPrintAllWithSpaces(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger()
t1 := &ast.Task{Task: "t1"}
t2 := &ast.Task{Task: "t2"}
t3 := &ast.Task{Task: "t3"}
tasks := ast.Tasks{}
tasks := ast.NewTasks()
tasks.Set("t1", t1)
tasks.Set("t2", t2)
tasks.Set("t3", t3)
summary.PrintTasks(&l,
&ast.Taskfile{Tasks: tasks},
[]*ast.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})
[]string{"t1", "t2", "t3"},
)
assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")

View File

@@ -1,6 +1,7 @@
package templater
import (
"maps"
"path/filepath"
"runtime"
"strings"
@@ -60,13 +61,9 @@ func init() {
cap += len(m)
}
result := make(map[string]any, cap)
for k, v := range base {
result[k] = v
}
maps.Copy(result, base)
for _, m := range v {
for k, v := range m {
result[k] = v
}
maps.Copy(result, m)
}
return result
},
@@ -84,7 +81,5 @@ func init() {
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = template.FuncMap(sprig.TxtFuncMap())
for k, v := range taskFuncs {
templateFuncs[k] = v
}
maps.Copy(templateFuncs, taskFuncs)
}

View File

@@ -6,9 +6,10 @@ import (
"maps"
"strings"
"github.com/go-task/template"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/template"
)
// Cache is a help struct that allow us to call "replaceX" funcs multiple
@@ -140,11 +141,10 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
return nil
}
var newVars ast.Vars
_ = vars.Range(func(k string, v ast.Var) error {
newVars := ast.NewVars()
for k, v := range vars.All() {
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
return nil
})
}
return &newVars
return newVars
}

View File

@@ -1,33 +1,67 @@
package version
import (
"fmt"
_ "embed"
"runtime/debug"
"strings"
)
var (
version = ""
sum = ""
//go:embed version.txt
version string
commit string
dirty bool
)
func init() {
info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" {
version = "unknown"
} else {
if version == "" {
version = info.Main.Version
}
if sum == "" {
sum = info.Main.Sum
}
version = strings.TrimSpace(version)
// Attempt to get build info from the Go runtime. We only use this if not
// built from a tagged version.
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version == "(devel)" {
commit = getCommit(info)
dirty = getDirty(info)
}
}
func getDirty(info *debug.BuildInfo) bool {
for _, setting := range info.Settings {
if setting.Key == "vcs.modified" {
return setting.Value == "true"
}
}
return false
}
func getCommit(info *debug.BuildInfo) string {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
return setting.Value[:7]
}
}
return ""
}
// GetVersion returns the version of Task. By default, this is retrieved from
// the embedded version.txt file which is kept up-to-date by our release script.
// However, it can also be overridden at build time using:
// -ldflags="-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'".
func GetVersion() string {
return version
}
func GetVersionWithSum() string {
return fmt.Sprintf("%s (%s)", version, sum)
// GetVersionWithBuildInfo is the same as [GetVersion], but it also includes
// the commit hash and dirty status if available. This will only work when built
// within inside of a Git checkout.
func GetVersionWithBuildInfo() string {
var buildMetadata []string
if commit != "" {
buildMetadata = append(buildMetadata, commit)
}
if dirty {
buildMetadata = append(buildMetadata, "dirty")
}
if len(buildMetadata) > 0 {
return version + "+" + strings.Join(buildMetadata, ".")
}
return version
}

View File

@@ -0,0 +1 @@
3.43.3

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.40.1",
"version": "3.43.3",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.40.1",
"version": "3.43.3",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",
@@ -22,7 +22,7 @@
"build-tool",
"task-runner"
],
"author": "Andrey Nering",
"author": "The Task authors",
"license": "MIT",
"bugs": {
"url": "https://github.com/go-task/task/issues"

View File

@@ -7,42 +7,54 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
}
vars, err := e.Compiler.GetVariables(t, call)
if err != nil {
return err
}
var missingVars []string
var notAllowedValuesVars []errors.NotAllowedVar
var missingVars []errors.MissingVar
for _, requiredVar := range t.Requires.Vars {
value, isString := vars.Get(requiredVar.Name).Value.(string)
if !vars.Exists(requiredVar.Name) {
missingVars = append(missingVars, requiredVar.Name)
} else {
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
_, ok := t.Vars.Get(requiredVar.Name)
if !ok {
missingVars = append(missingVars, errors.MissingVar{
Name: requiredVar.Name,
AllowedValues: requiredVar.Enum,
})
}
}
if len(missingVars) > 0 {
return &errors.TaskMissingRequiredVars{
return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(),
MissingVars: missingVars,
}
}
return nil
}
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
}
var notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars {
varValue, _ := t.Vars.Get(requiredVar.Name)
value, isString := varValue.Value.(string)
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
}
if len(notAllowedValuesVars) > 0 {
return &errors.TaskNotAllowedVars{
return &errors.TaskNotAllowedVarsError{
TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars,
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
@@ -55,7 +55,7 @@ func (e *Executor) Setup() error {
}
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if err != nil {
return nil, err
}
@@ -64,17 +64,28 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
defer cf()
debugFunc := func(s string) {
e.Logger.VerboseOutf(logger.Magenta, s)
}
promptFunc := func(s string) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader(
node,
e.Insecure,
e.Download,
e.Offline,
e.Timeout,
e.TempDir.Remote,
e.Logger,
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
graph, err := reader.Read()
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
}
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
@@ -92,12 +103,9 @@ func (e *Executor) setupFuzzyModel() {
model.SetThreshold(1) // because we want to build grammar based on every task name
var words []string
for _, taskName := range e.Taskfile.Tasks.Keys() {
words = append(words, taskName)
for _, task := range e.Taskfile.Tasks.Values() {
words = slices.Concat(words, task.Aliases)
}
for name, task := range e.Taskfile.Tasks.All(nil) {
words = append(words, name)
words = slices.Concat(words, task.Aliases)
}
model.Train(words)
@@ -109,13 +117,14 @@ func (e *Executor) setupTempDir() error {
return nil
}
if os.Getenv("TASK_TEMP_DIR") == "" {
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return err
}
@@ -128,14 +137,15 @@ func (e *Executor) setupTempDir() error {
} else {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
}
}
if os.Getenv("TASK_REMOTE_DIR") != "" {
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") {
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR"))
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
@@ -191,7 +201,7 @@ func (e *Executor) setupCompiler() error {
}
}
e.Compiler = &compiler.Compiler{
e.Compiler = &Compiler{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
UserWorkingDir: e.UserWorkingDir,
@@ -203,21 +213,29 @@ func (e *Executor) setupCompiler() error {
}
func (e *Executor) readDotEnvFiles() error {
if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {
return nil
}
if e.Taskfile.Version.LessThan(ast.V3) {
return nil
}
env, err := taskfile.Dotenv(e.Compiler, e.Taskfile, e.Dir)
vars, err := e.Compiler.GetTaskfileVariables()
if err != nil {
return err
}
err = env.Range(func(key string, value ast.Var) error {
if ok := e.Taskfile.Env.Exists(key); !ok {
e.Taskfile.Env.Set(key, value)
env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)
if err != nil {
return err
}
for k, v := range env.All() {
if _, ok := e.Taskfile.Env.Get(k); !ok {
e.Taskfile.Env.Set(k, v)
}
return nil
})
}
return err
}
@@ -235,7 +253,7 @@ func (e *Executor) setupConcurrencyState() {
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
for _, k := range e.Taskfile.Tasks.Keys() {
for k := range e.Taskfile.Tasks.Keys(nil) {
e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{}
}
@@ -246,6 +264,9 @@ func (e *Executor) setupConcurrencyState() {
}
func (e *Executor) doVersionChecks() error {
if !e.EnableVersionCheck {
return nil
}
// Copy the version to avoid modifying the original
schemaVersion := &semver.Version{}
*schemaVersion = *e.Taskfile.Version

View File

@@ -8,20 +8,20 @@ import (
"github.com/go-task/task/v3/internal/logger"
)
const interruptSignalsCount = 3
const maxInterruptSignals = 3
// NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals
// so the Task process is not killed immediately and processes running have
// time to do cleanup work.
func (e *Executor) InterceptInterruptSignals() {
ch := make(chan os.Signal, interruptSignalsCount)
ch := make(chan os.Signal, maxInterruptSignals)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() {
for i := range interruptSignalsCount {
for i := range maxInterruptSignals {
sig := <-ch
if i+1 >= interruptSignalsCount {
if i+1 >= maxInterruptSignals {
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
os.Exit(1)
}

View File

@@ -9,7 +9,7 @@ import (
)
// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...*ast.Call) error {
func (e *Executor) Status(ctx context.Context, calls ...*Call) error {
for _, call := range calls {
// Compile the task

156
task.go
View File

@@ -3,18 +3,15 @@ package task
import (
"context"
"fmt"
"io"
"os"
"runtime"
"slices"
"sync"
"sync/atomic"
"time"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/fingerprint"
@@ -25,9 +22,6 @@ import (
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/sajari/fuzzy"
"golang.org/x/sync/errgroup"
)
const (
@@ -36,58 +30,15 @@ const (
MaximumTaskCall = 1000
)
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile
type Executor struct {
Taskfile *ast.Taskfile
Dir string
Entrypoint string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
Download bool
Offline bool
Timeout time.Duration
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Logger *logger.Logger
Compiler *compiler.Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.TaskSorter
UserWorkingDir string
fuzzyModel *fuzzy.Model
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
// MatchingTask represents a task that matches a given call. It includes the
// task itself and a list of wildcards that were matched.
type MatchingTask struct {
Task *ast.Task
Wildcards []string
}
// Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
// check if given tasks exist
for _, call := range calls {
task, err := e.GetTask(call)
@@ -149,7 +100,7 @@ func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
return nil
}
func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) {
func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Call, watchCalls []*Call, err error) {
for _, c := range calls {
t, err := e.GetTask(c)
if err != nil {
@@ -166,7 +117,7 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls [
}
// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
func (e *Executor) RunTask(ctx context.Context, call *Call) error {
t, err := e.FastCompiledTask(call)
if err != nil {
return err
@@ -176,10 +127,19 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return nil
}
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
t, err = e.CompiledTask(call)
if err != nil {
return err
}
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err
}
if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {
return &errors.TaskCalledTooManyTimesError{
TaskName: t.Task,
@@ -202,10 +162,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return err
}
if err := e.areTaskRequiredVarsSet(t, call); err != nil {
return err
}
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil {
return err
@@ -311,7 +267,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
for _, d := range t.Deps {
d := d
g.Go(func() error {
err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil {
return err
}
@@ -322,7 +278,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait()
}
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -341,13 +297,15 @@ func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitC
}
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil {
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
}
}
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
cmd := t.Cmds[i]
switch {
@@ -355,7 +313,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil {
return err
}
@@ -383,7 +341,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err)
}
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
stdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd,
@@ -395,7 +353,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
Stdout: stdOut,
Stderr: stdErr,
})
if closeErr := close(err); closeErr != nil {
if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
}
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
@@ -441,35 +399,50 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return execute(ctx)
}
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
}
// Attempt a wildcard match
for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match {
matchingTasks = append(matchingTasks, &MatchingTask{
Task: value,
Wildcards: wildcards,
})
}
}
return matchingTasks
}
// GetTask will return the task with the name matching the given call from the taskfile.
// If no task is found, it will search for tasks with a matching alias.
// If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// Search for a matching task
matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
switch len(matchingTasks) {
case 0: // Carry on
case 1:
matchingTasks := e.FindMatchingTasks(call)
if len(matchingTasks) > 0 {
if call.Vars == nil {
call.Vars = &ast.Vars{}
call.Vars = ast.NewVars()
}
call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
return matchingTasks[0].Task, nil
default:
taskNames := make([]string, len(matchingTasks))
for i, matchingTask := range matchingTasks {
taskNames[i] = matchingTask.Task.Task
}
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: taskNames,
}
}
// If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task
var aliasedTasks []string
for _, task := range e.Taskfile.Tasks.Values() {
for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task
@@ -505,8 +478,13 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Create an error group to wait for each task to be compiled
var g errgroup.Group
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
}
// Filter tasks based on the given filter functions
for _, task := range e.Taskfile.Tasks.Values() {
for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
var shouldFilter bool
for _, filter := range filters {
if filter(task) {
@@ -521,7 +499,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks
for i := range tasks {
g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
if err != nil {
return err
}
@@ -535,12 +513,6 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
return nil, err
}
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
}
e.TaskSorter.Sort(tasks)
return tasks, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -51,18 +51,53 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil
case yaml.MappingNode:
// A command with additional options
var cmdStruct struct {
Cmd string
Task string
For *For
Silent bool
Set []string
Shopt []string
Vars *Vars
IgnoreError bool `yaml:"ignore_error"`
Defer *Defer
Platforms []*Platform
}
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if cmdStruct.Defer != nil {
// A deferred command
if cmdStruct.Defer.Cmd != "" {
c.Defer = true
c.Cmd = cmdStruct.Defer.Cmd
c.Silent = cmdStruct.Silent
return nil
}
// A deferred task call
if cmdStruct.Defer.Task != "" {
c.Defer = true
c.Task = cmdStruct.Defer.Task
c.Vars = cmdStruct.Defer.Vars
c.Silent = cmdStruct.Defer.Silent
return nil
}
return nil
}
// A task call
if cmdStruct.Task != "" {
c.Task = cmdStruct.Task
c.Vars = cmdStruct.Vars
c.For = cmdStruct.For
c.Silent = cmdStruct.Silent
return nil
}
// A command with additional options
if cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.For = cmdStruct.For
c.Silent = cmdStruct.Silent
@@ -73,42 +108,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil
}
// A deferred command
var deferredCmd struct {
Defer string
}
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true
c.Cmd = deferredCmd.Defer
return nil
}
// A deferred task call
var deferredCall struct {
Defer Call
}
if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
c.Defer = true
c.Task = deferredCall.Defer.Task
c.Vars = deferredCall.Defer.Vars
return nil
}
// A task call
var taskCall struct {
Task string
Vars *Vars
For *For
Silent bool
}
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
c.Task = taskCall.Task
c.Vars = taskCall.Vars
c.For = taskCall.For
c.Silent = taskCall.Silent
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
}

45
taskfile/ast/defer.go Normal file
View File

@@ -0,0 +1,45 @@
package ast
import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
type Defer struct {
Cmd string
Task string
Vars *Vars
Silent bool
}
func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
d.Cmd = cmd
return nil
case yaml.MappingNode:
var deferStruct struct {
Defer string
Task string
Vars *Vars
Silent bool
}
if err := node.Decode(&deferStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
d.Cmd = deferStruct.Defer
d.Task = deferStruct.Task
d.Vars = deferStruct.Vars
d.Silent = deferStruct.Silent
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer")
}

View File

@@ -5,13 +5,12 @@ import (
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/omap"
)
type For struct {
From string
List []any
Matrix omap.OrderedMap[string, []any]
Matrix *Matrix
Var string
Split string
As string
@@ -38,7 +37,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
var forStruct struct {
Matrix omap.OrderedMap[string, []any]
Matrix *Matrix
Var string
Split string
As string

View File

@@ -116,14 +116,5 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return nil, err
}
_ = rootVertex.Taskfile.Tasks.Range(func(name string, task *Task) error {
if task == nil {
task = &Task{}
rootVertex.Taskfile.Tasks.Set(name, task)
}
task.Task = name
return nil
})
return rootVertex.Taskfile, nil
}

View File

@@ -1,46 +1,141 @@
package ast
import (
"iter"
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
omap "github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Include represents information about included taskfiles
type Include struct {
Namespace string
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
AdvancedImport bool
Vars *Vars
Flatten bool
type (
// Include represents information about included taskfiles
Include struct {
Namespace string
Taskfile string
Dir string
Optional bool
Internal bool
Aliases []string
Excludes []string
AdvancedImport bool
Vars *Vars
Flatten bool
}
// Includes is an ordered map of namespaces to includes.
Includes struct {
om *orderedmap.OrderedMap[string, *Include]
mutex sync.RWMutex
}
// An IncludeElement is a key-value pair that is used for initializing an
// Includes structure.
IncludeElement orderedmap.Element[string, *Include]
)
// NewIncludes creates a new instance of Includes and initializes it with the
// provided set of elements, if any. The elements are added in the order they
// are passed.
func NewIncludes(els ...*IncludeElement) *Includes {
includes := &Includes{
om: orderedmap.NewOrderedMap[string, *Include](),
}
for _, el := range els {
includes.Set(el.Key, el.Value)
}
return includes
}
// Includes represents information about included tasksfiles
type Includes struct {
omap.OrderedMap[string, *Include]
// Len returns the number of includes in the Includes map.
func (includes *Includes) Len() int {
if includes == nil || includes.om == nil {
return 0
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Len()
}
// Get returns the value the the include with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned include is a zero value and the bool is false.
func (includes *Includes) Get(key string) (*Include, bool) {
if includes == nil || includes.om == nil {
return &Include{}, false
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Get(key)
}
// Set sets the value of the include with the provided key to the provided
// value. If the include already exists, its value is updated. If the include
// does not exist, it is created.
func (includes *Includes) Set(key string, value *Include) bool {
if includes == nil {
includes = NewIncludes()
}
if includes.om == nil {
includes.om = orderedmap.NewOrderedMap[string, *Include]()
}
defer includes.mutex.Unlock()
includes.mutex.Lock()
return includes.om.Set(key, value)
}
// All returns an iterator that loops over all task key-value pairs.
// Range calls the provided function for each include in the map. The function
// receives the include's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (includes *Includes) All() iter.Seq2[string, *Include] {
if includes == nil || includes.om == nil {
return func(yield func(string, *Include) bool) {}
}
return includes.om.AllFromFront()
}
// Keys returns an iterator that loops over all task keys.
func (includes *Includes) Keys() iter.Seq[string] {
if includes == nil || includes.om == nil {
return func(yield func(string) bool) {}
}
return includes.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (includes *Includes) Values() iter.Seq[*Include] {
if includes == nil || includes.om == nil {
return func(yield func(*Include) bool) {}
}
return includes.om.Values()
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
if includes == nil || includes.om == nil {
*includes = *NewIncludes()
}
switch node.Kind {
case yaml.MappingNode:
// NOTE(@andreynering): on this style of custom unmarshalling,
// even number contains the keys, while odd numbers contains
// the values.
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// Decode the value node into an Include struct
var v Include
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Set the include namespace
v.Namespace = keyNode.Value
// Add the include to the ordered map
includes.Set(keyNode.Value, &v)
}
return nil
@@ -49,22 +144,6 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
}
// Len returns the length of the map
func (includes *Includes) Len() int {
if includes == nil {
return 0
}
return includes.OrderedMap.Len()
}
// 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
}
return includes.OrderedMap.Range(f)
}
func (include *Include) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
@@ -84,6 +163,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Internal bool
Flatten bool
Aliases []string
Excludes []string
Vars *Vars
}
if err := node.Decode(&includedTaskfile); err != nil {
@@ -94,6 +174,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.Optional = includedTaskfile.Optional
include.Internal = includedTaskfile.Internal
include.Aliases = includedTaskfile.Aliases
include.Excludes = includedTaskfile.Excludes
include.AdvancedImport = true
include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
@@ -115,6 +196,7 @@ func (include *Include) DeepCopy() *Include {
Dir: include.Dir,
Optional: include.Optional,
Internal: include.Internal,
Excludes: deepcopy.Slice(include.Excludes),
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,

141
taskfile/ast/matrix.go Normal file
View File

@@ -0,0 +1,141 @@
package ast
import (
"iter"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
type (
// Matrix is an ordered map of variable names to arrays of values.
Matrix struct {
om *orderedmap.OrderedMap[string, *MatrixRow]
}
// A MatrixElement is a key-value pair that is used for initializing a
// Matrix structure.
MatrixElement orderedmap.Element[string, *MatrixRow]
// A MatrixRow list of values for a matrix key or a reference to another
// variable.
MatrixRow struct {
Ref string
Value []any
}
)
func NewMatrix(els ...*MatrixElement) *Matrix {
matrix := &Matrix{
om: orderedmap.NewOrderedMap[string, *MatrixRow](),
}
for _, el := range els {
matrix.Set(el.Key, el.Value)
}
return matrix
}
func (matrix *Matrix) Len() int {
if matrix == nil || matrix.om == nil {
return 0
}
return matrix.om.Len()
}
func (matrix *Matrix) Get(key string) (*MatrixRow, bool) {
if matrix == nil || matrix.om == nil {
return nil, false
}
return matrix.om.Get(key)
}
func (matrix *Matrix) Set(key string, value *MatrixRow) bool {
if matrix == nil {
matrix = NewMatrix()
}
if matrix.om == nil {
matrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]()
}
return matrix.om.Set(key, value)
}
// All returns an iterator that loops over all task key-value pairs.
func (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] {
if matrix == nil || matrix.om == nil {
return func(yield func(string, *MatrixRow) bool) {}
}
return matrix.om.AllFromFront()
}
// Keys returns an iterator that loops over all task keys.
func (matrix *Matrix) Keys() iter.Seq[string] {
if matrix == nil || matrix.om == nil {
return func(yield func(string) bool) {}
}
return matrix.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (matrix *Matrix) Values() iter.Seq[*MatrixRow] {
if matrix == nil || matrix.om == nil {
return func(yield func(*MatrixRow) bool) {}
}
return matrix.om.Values()
}
func (matrix *Matrix) DeepCopy() *Matrix {
if matrix == nil {
return nil
}
return &Matrix{
om: deepcopy.OrderedMap(matrix.om),
}
}
func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
switch valueNode.Kind {
case yaml.SequenceNode:
// Decode the value node into a Matrix struct
var v []any
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the row to the ordered map
matrix.Set(keyNode.Value, &MatrixRow{
Value: v,
})
case yaml.MappingNode:
// Decode the value node into a Matrix struct
var refStruct struct {
Ref string
}
if err := valueNode.Decode(&refStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the reference to the ordered map
matrix.Set(keyNode.Value, &MatrixRow{
Ref: refStruct.Ref,
})
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("matrix values must be an array or a reference")
}
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("matrix")
}

View File

@@ -8,6 +8,8 @@ import (
)
func TestPlatformParsing(t *testing.T) {
t.Parallel()
tests := []struct {
Input string
ExpectedOS string
@@ -34,6 +36,8 @@ func TestPlatformParsing(t *testing.T) {
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
t.Parallel()
var p Platform
err := p.parsePlatform(test.Input)

View File

@@ -11,6 +11,8 @@ import (
)
func TestPreconditionParse(t *testing.T) {
t.Parallel()
tests := []struct {
content string
v any

View File

@@ -29,7 +29,7 @@ type Taskfile struct {
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
Tasks *Tasks
Silent bool
Dotenv []string
Run string
@@ -47,11 +47,17 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t2.Output.IsSet() {
t1.Output = t2.Output
}
if t1.Includes == nil {
t1.Includes = NewIncludes()
}
if t1.Vars == nil {
t1.Vars = &Vars{}
t1.Vars = NewVars()
}
if t1.Env == nil {
t1.Env = &Vars{}
t1.Env = NewVars()
}
if t1.Tasks == nil {
t1.Tasks = NewTasks()
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
@@ -70,7 +76,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Shopt []string
Vars *Vars
Env *Vars
Tasks Tasks
Tasks *Tasks
Silent bool
Dotenv []string
Run string
@@ -92,11 +98,17 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
if tf.Includes == nil {
tf.Includes = NewIncludes()
}
if tf.Vars == nil {
tf.Vars = &Vars{}
tf.Vars = NewVars()
}
if tf.Env == nil {
tf.Env = &Vars{}
tf.Env = NewVars()
}
if tf.Tasks == nil {
tf.Tasks = NewTasks()
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More