Compare commits

...

165 Commits

Author SHA1 Message Date
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
Andrey Nering
32fa3a0156 v3.40.1 2024-12-06 09:59:04 -03:00
dependabot[bot]
973e928c28 chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /website (#1934)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-30 12:01:46 -03:00
Oleksandr Redko
bc844246d4 chore: allow using only github.com/go-task/task/v3/errors package (#1926) 2024-11-30 12:00:58 -03:00
dependabot[bot]
41884f0a69 chore(deps): bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1 (#1927)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.0...v3.3.1)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-30 11:58:17 -03:00
dependabot[bot]
2a96c20739 chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#1928)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-30 11:58:00 -03:00
dependabot[bot]
c28eb204fb chore(deps): bump golang.org/x/term from 0.25.0 to 0.26.0 (#1913)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 21:54:17 +01:00
dependabot[bot]
b1535aedc1 chore(deps): bump golang.org/x/sync from 0.8.0 to 0.9.0 (#1914)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 21:50:03 +01:00
Valentin Maerten
7e3feb2993 chore: changelog for #1883 2024-11-15 21:49:21 +01:00
christiandins
8a79a41717 chore: add linter misspell (#1883) 2024-11-15 21:48:59 +01:00
Valentin Maerten
530818a742 chore: changelog for #1915 2024-11-15 21:42:15 +01:00
Dorian Karter
517bb3fc97 fix(json-schema): add missing platforms property to cmds for (#1915) 2024-11-15 21:41:54 +01:00
Valentin Maerten
6645a1f34c chore: changelog for #1917 2024-11-12 11:20:15 +01:00
Alexey Palazhchenko
2aa2963565 chore(deps): switch to mainted fork of git-urls (#1917) 2024-11-12 09:29:29 +01:00
Oleksandr Redko
390220ec9c fix: typos in docs, changelog, testdata, comments (#1910) 2024-11-07 13:25:59 -03:00
Andrey Nering
c3bd6b9384 chore(taskfile): fix goreleaser:install task to install correct version 2024-11-05 23:21:24 -03:00
Andrey Nering
d8e176311d v3.40.0 2024-11-05 22:34:38 -03:00
dependabot[bot]
1c68f0fee4 chore(deps): bump http-proxy-middleware from 2.0.6 to 2.0.7 in /website (#1886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 19:58:26 +01:00
Pete Davison
118ef01a69 chore: changelog for #1904 2024-11-04 13:32:47 +00:00
Pete Davison
148b090d8e fix: bug where non-nil, empty dynamic variables are returned as an empty interface (#1904) 2024-11-04 13:30:39 +00:00
Norbert Hauriel
28a96d1427 docs(flags.go): flag description typo (#1905) 2024-11-04 12:58:48 +00:00
George Green
47f5e6ab89 docs: add an example of a default value usage in vars (#1893) 2024-11-01 20:04:46 +01:00
renovate[bot]
fe09c01637 chore(deps): update website (#1891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 15:31:56 +01:00
dependabot[bot]
7ef3164b16 chore(deps): bump github.com/fatih/color from 1.17.0 to 1.18.0 (#1885)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 15:00:57 +01:00
dependabot[bot]
b48a32b103 chore(deps): bump github.com/go-git/go-billy/v5 from 5.5.0 to 5.6.0 (#1884)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 14:52:53 +01:00
Valentin Maerten
2d2c408652 chore: changelog for #1890 2024-10-29 14:50:34 +01:00
Amogh Rameshappa Devapura
c381923d3e feat: add numCPU func (#1890) 2024-10-29 14:50:17 +01:00
Pete Davison
7bfddaa25a chore: changelog for #1866 2024-10-29 13:39:04 +00:00
Matheus Mina
5581954fb1 feat: allow providing single or multi prompts (#1866)
* Add new type to handle single or multi prompts

* update docs

* apply review
2024-10-29 13:37:03 +00:00
renovate[bot]
c4f708b222 fix(deps): update module mvdan.cc/sh/v3 to v3.10.0 (#1874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 19:52:54 +02:00
Valentin Maerten
27056a9827 chore: changelog for #1827 2024-10-18 18:17:18 +02:00
Valentin Maerten
a35910429c feat: option to ensure variable is within the list of values (#1827) 2024-10-18 18:16:57 +02:00
Valentin Maerten
9a7e79258c chore: changelog for #1771 2024-10-18 18:14:07 +02:00
Paulo Bittencourt
8dd3f4b119 refactor: re-organize node loading code to make it easier to follow (#1771) 2024-10-18 18:13:25 +02:00
Valentin Maerten
9ecc8fc878 chore: changelog for #1810 2024-10-09 09:14:56 +02:00
Valentin Maerten
e078261f12 fix: special variables are defined with dotenv at task level (#1810) 2024-10-09 03:14:23 -04:00
Andrey Nering
bdb3ffddd1 chore: add changelog for #1757 2024-10-05 21:42:35 -03:00
Paulo Bittencourt
a72e70b026 fix: inconsistent current directory resolution depending on include order (#1757) 2024-10-05 21:40:22 -03:00
Paulo Bittencourt
c5eea294aa ci: fix flaky TestForDeps tests (#1839) 2024-10-05 21:25:12 -03:00
renovate[bot]
0fff404eb8 chore(deps): update goreleaser/goreleaser-action action to v6 (#1852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 00:21:52 +00:00
renovate[bot]
61172fa8da chore(deps): update dependency @types/react to v18.3.11 (#1851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 21:20:21 -03:00
dependabot[bot]
a6bc3f51cc chore(deps): bump golang.org/x/term from 0.24.0 to 0.25.0 (#1857)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/term/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 21:19:15 -03:00
renovate[bot]
1af7bf2670 chore(deps): update actions/github-script action to v7 (#1849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 03:27:06 -04:00
dependabot[bot]
d75536bf00 chore(deps): bump express from 4.19.2 to 4.21.0 in /website (#1815)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-29 16:08:03 -04:00
Valentin Maerten
ce3e058f89 chore: changelog for #1842 2024-09-29 22:05:33 +02:00
Paulo Bittencourt
8d0f0b049c fix: Print dotenv file path when there is an error reading file (#1842) 2024-09-29 16:03:48 -04:00
Valentin Maerten
e619bad4a9 chore: changelog for #1652 2024-09-24 19:45:59 +02:00
Valentin Maerten
e6ea0647d7 feat(remote): support include git remote (#1652) 2024-09-24 13:44:54 -04:00
renovate[bot]
d1dc271b9a chore(deps): update website (#1834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 13:26:36 -04:00
Valentin Maerten
f5082f3692 chore: changelog for #1833 2024-09-24 19:23:18 +02:00
Valentin Maerten
30c59bf387 fix(remote): wait for prompt in the reader (#1833) 2024-09-24 13:21:09 -04:00
renovate[bot]
38d0fc2c55 chore(deps): update tj-actions/changed-files action to v45 (#1835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 15:31:51 -04:00
George Rawlinson
460e587c66 fix: checksum override when passed via ldflags (#1830) 2024-09-23 13:45:41 -04:00
Valentin Maerten
ad5a3166ac chore: changelog for #1716 2024-09-21 17:24:33 +02:00
Valentin Maerten
ddccd1bb61 feat: add TASK_OFFLINE env and expose it as a special variable (#1716)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2024-09-21 11:17:15 -04:00
Pete Davison
96a690ac2f chore: changelog for #1822 2024-09-20 17:08:54 +00:00
Piotr Stawarski
cb07189bab Fix: Cannot use splitArgs and splitLines in for-loops (#1823)
* Update variables.go

Probably solves https://github.com/go-task/task/issues/1822

* add type casting

* reorder to look better

* add suport for []int functions (until, untilStep)
2024-09-20 18:05:19 +01:00
Carlos Alexandro Becker
7e6577eb5f fix: snapshot builds and wrong winget version (#1824) 2024-09-20 10:13:42 -03:00
Pete Davison
58ab26c4ab v3.39.2 2024-09-19 12:25:29 +00:00
Valentin Maerten
65d332dfd0 chore: changelog for #1818 2024-09-19 14:24:35 +02:00
Valentin Maerten
5eaf0b2dcd fix: interpolate dynamic vars in defer (#1818) 2024-09-19 08:22:39 -04:00
228 changed files with 10337 additions and 6201 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

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## 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.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at task@taskfile.dev. 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.

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

@@ -8,7 +8,7 @@ jobs:
issue-awaiting-response:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-closed:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |

View File

@@ -9,7 +9,7 @@ jobs:
if: github.event.label.name == format('experiment{0} proposed', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -23,7 +23,7 @@ jobs:
if: github.event.label.name == format('experiment{0} draft', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -37,7 +37,7 @@ jobs:
if: github.event.label.name == format('experiment{0} candidate', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -51,7 +51,7 @@ jobs:
if: github.event.label.name == format('experiment{0} stable', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -65,7 +65,7 @@ jobs:
if: github.event.label.name == format('experiment{0} released', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -85,7 +85,7 @@ jobs:
if: github.event.label.name == format('experiment{0} abandoned', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |
@@ -105,7 +105,7 @@ jobs:
if: github.event.label.name == format('experiment{0} superseded', ':')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-needs-triage:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v7
with:
github-token: ${{secrets.GH_PAT}}
script: |

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
@@ -25,7 +25,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60.1
version: v1.64.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@v44
uses: tj-actions/changed-files@v45
with:
files: website/versioned_docs/**

View File

@@ -15,10 +15,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.23.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean

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

@@ -5,13 +5,33 @@
linters:
enable:
- depguard
- goimports
- gofmt
- gofumpt
- mirror
- misspell
- noctx
- paralleltest
- usetesting
- thelper
- tparallel
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
gofumpt:
module-path: github.com/go-task/task/v3
gofmt:
rewrite-rules:
- pattern: 'interface{}'

View File

@@ -49,7 +49,7 @@ release:
draft: true
snapshot:
version_template: "{{.Tag}}"
version_template: "{{.Version}}"
checksum:
name_template: "task_checksums.txt"
@@ -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:

2
.nvmrc
View File

@@ -1 +1 @@
18.12.1
22.14.0

View File

@@ -1,5 +1,156 @@
# Changelog
## 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).
## v3.40.0 - 2024-11-05
- Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in
for loops (#1822, #1823 by @stawii).
- Added a new `TASK_OFFLINE` environment variable to configure the `--offline`
flag and expose it as a special variable in the templating system (#1470,
#1716 by @vmaerten and @pd93).
- Fixed a bug where multiple remote includes caused all prompts to display
without waiting for user input (#1832, #1833 by @vmaerten and @pd93).
- When using the
"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)".
experiment, you can now include Taskfiles from Git repositories (#1652 by
@vmaerten).
- Improved the error message when a dotenv file cannot be parsed (#1842 by
@pbitty).
- Fix issue with directory when using the remote experiment (#1757 by @pbitty).
- Fixed an issue where a special variable was used in combination with a dotenv
file (#1232, #1810 by @vmaerten).
- Refactor the way Task reads Taskfiles to improve readability (#1771 by
@pbitty).
- Added a new option to ensure variable is within the list of values (#1827 by
@vmaerten).
- Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina).
- Added new template function: `numCPU`, which returns the number of logical
CPUs usable (#1890, #1887 by @Amoghrd).
- Fixed a bug where non-nil, empty dynamic variables are returned as an empty
interface (#1903, #1904 by @pd93).
## v3.39.2 - 2024-09-19
- Fix dynamic variables not working properly for a defer: statement (#1803,
#1818 by @vmaerten).
## v3.39.1 - 2024-09-18
- Added Renovate configuration to automatically create PRs to keep dependencies
@@ -259,8 +410,8 @@
- Added the
[Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
as a draft (#1152, #1317 by @pd93).
- Improve performance of content checksuming 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).
@@ -319,7 +470,7 @@
- Deprecated `version: 2` schema. This will be removed in the next major release
(#1197, #1198, #1199 by @pd93).
- Added a new `prompt:` prop to set a warning prompt to be shown before running
a potential dangurous task (#100, #1163 by @MaxCheetham,
a potential dangerous task (#100, #1163 by @MaxCheetham,
[Documentation](https://taskfile.dev/usage/#warning-prompts)).
- Added support for single command task syntax. With this change, it's now
possible to declare just `cmd:` in a task, avoiding the more complex
@@ -334,7 +485,7 @@
percentage (#1173 by @misitebao).
- Starting on this release, official binaries for FreeBSD will be available to
download (#1068 by @andreynering).
- Fix some errors being unintendedly supressed (#1134 by @clintmod).
- Fix some errors being unintendedly suppressed (#1134 by @clintmod).
- Fix a nil pointer error when `version` is omitted from a Taskfile (#1148,
#1149 by @pd93).
- Fix duplicate error message when a task does not exists (#1141, #1144 by
@@ -407,8 +558,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.
Usefull 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
@@ -763,7 +914,7 @@ it a go and let us know what you think via a
- Fix error code for the `--help` flag (#300, #330).
- Print version to stdout instead of stderr (#299, #329).
- Supress `context` errors when using the `--watch` flag (#313, #317).
- Suppress `context` errors when using the `--watch` flag (#313, #317).
- Support templating on description (#276, #283).
## v2.8.0 - 2019-12-07
@@ -772,7 +923,7 @@ it a go and let us know what you think via a
parallel (#266).
- Fixed bug where calling the `task` CLI only informing global vars would not
execute the `default` task.
- Add hability to silent all tasks by adding `silent: true` a the root of the
- Add ability to silent all tasks by adding `silent: true` a the root of the
Taskfile.
## v2.7.1 - 2019-11-10
@@ -914,7 +1065,7 @@ document, since it describes in depth what changed for this version.
## v1.4.3 - 2017-09-07
- Allow assigning variables to tasks at run time via CLI (#33)
- Added suport for multiline variables from sh (#64)
- Added support for multiline variables from sh (#64)
- Fixes env: remove square braces and evaluate shell (#62)
- Watch: change watch library and few fixes and improvements
- When use watching, cancel and restart long running process on file change (#59
@@ -974,7 +1125,7 @@ document, since it describes in depth what changed for this version.
- More tests and Travis integration
- Watch a task (experimental)
- Possibility to call another task
- Fix "=" not being reconized in variables/environment variables
- Fix "=" not being recognized in variables/environment variables
- Tasks can now have a description, and help will print them (#10)
- Task dependencies now run concurrently
- Support for a default task (#16)

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

@@ -98,21 +98,17 @@ 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: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
@@ -122,7 +118,23 @@ tasks:
goreleaser:install:
desc: Installs goreleaser
cmds:
- go install github.com/goreleaser/goreleaser@latest
- 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
@@ -176,11 +188,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,18 @@ package args
import (
"strings"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/taskfile/ast"
)
// 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

@@ -1,7 +1,6 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
@@ -12,6 +11,8 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/otiai10/copy"
"github.com/spf13/pflag"
"github.com/go-task/task/v3/errors"
)
const (

View File

@@ -159,7 +159,7 @@ func worker(
return workerDone
}
// Do some work and then return, so that the caller can decide wether to continue or not.
// Do some work and then return, so that the caller can decide whether to continue or not.
// Return true when all work is done.
func doSomeWork(deadline time.Time) bool {
if time.Now().After(deadline) {

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/pflag"
@@ -13,6 +14,7 @@ import (
"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/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"
@@ -44,7 +46,7 @@ func main() {
}
func run() error {
logger := &logger.Logger{
log := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
@@ -69,7 +71,7 @@ func run() error {
}
if flags.Experiments {
return experiments.List(logger)
return log.PrintExperiments()
}
if flags.Init {
@@ -77,9 +79,28 @@ func run() error {
if err != nil {
return err
}
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
args, _, err := getArgs()
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
}
@@ -100,12 +121,16 @@ func run() error {
dir = home
}
var taskSorter sort.TaskSorter
if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error())
}
var taskSorter sort.Sorter
switch flags.TaskSort {
case "none":
taskSorter = &sort.Noop{}
taskSorter = nil
case "alphanumeric":
taskSorter = &sort.AlphaNumeric{}
taskSorter = sort.AlphaNumeric
}
e := task.Executor{
@@ -132,8 +157,9 @@ func run() error {
Stdout: os.Stdout,
Stderr: os.Stderr,
OutputStyle: flags.Output,
TaskSorter: taskSorter,
OutputStyle: flags.Output,
TaskSorter: taskSorter,
EnableVersionCheck: true,
}
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil {
@@ -144,9 +170,6 @@ func run() error {
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
@@ -178,7 +201,7 @@ func run() error {
}
var (
calls []*ast.Call
calls []*task.Call
globals *ast.Vars
)
@@ -191,13 +214,14 @@ func run() error {
// 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})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch {

View File

@@ -1,4 +1,4 @@
package compiler
package task
import (
"bytes"
@@ -9,6 +9,7 @@ import (
"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"
@@ -35,24 +36,22 @@ 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()
if t != nil {
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
return nil, err
}
for k, v := range specialVars {
result.Set(k, ast.Var{Value: v})
}
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
}
for k, v := range specialVars {
result.Set(k, ast.Var{Value: v})
}
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
@@ -75,13 +74,13 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
if err := cache.Err(); err != nil {
return err
}
// If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" {
// If the variable is already set, we can set it and return
if newVar.Value != 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
}
@@ -104,43 +103,60 @@ 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()
// If the variable is not dynamic or it is empty, return an empty string
if v.Sh == nil || *v.Sh == "" {
return "", nil
}
if c.dynamicCache == nil {
c.dynamicCache = make(map[string]string, 30)
}
if result, ok := c.dynamicCache[v.Sh]; ok {
if result, ok := c.dynamicCache[*v.Sh]; ok {
return result, nil
}
@@ -151,10 +167,11 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Command: *v.Sh,
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)
@@ -165,13 +182,13 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
result := strings.TrimSuffix(stdout.String(), "\r\n")
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.dynamicCache[*v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
return result, nil
}
// ResetCache clear the dymanic variables cache
// ResetCache clear the dynamic variables cache
func (c *Compiler) ResetCache() {
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
@@ -179,16 +196,23 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil
}
func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
return map[string]string{
"TASK": t.Task,
"ALIAS": call.Task,
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),
"ROOT_DIR": c.Dir,
"TASKFILE": t.Location.Taskfile,
"TASKFILE_DIR": filepath.Dir(t.Location.Taskfile),
"USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(),
}, nil
}
if t != nil {
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)
}
if call != nil {
allVars["ALIAS"] = call.Task
}
return allVars, nil
}

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

@@ -32,6 +32,7 @@ const (
CodeTaskCalledTooManyTimes
CodeTaskCancelled
CodeTaskMissingRequiredVars
CodeTaskNotAllowedVars
)
// TaskError extends the standard error interface with a Code method. This code will

View File

@@ -141,20 +141,62 @@ 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
}
type NotAllowedVar struct {
Value string
Enum []string
Name string
}
type TaskNotAllowedVarsError struct {
TaskName string
NotAllowedVars []NotAllowedVar
}
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))
for _, s := range err.NotAllowedVars {
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
}
return builder.String()
}
func (err *TaskNotAllowedVarsError) Code() int {
return CodeTaskNotAllowedVars
}

51
go.mod
View File

@@ -1,39 +1,62 @@
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.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.15.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/fatih/color v1.17.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.2
github.com/go-git/go-git/v5 v5.14.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/otiai10/copy v1.14.1
github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
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.8.0
golang.org/x/term v0.24.0
golang.org/x/sync v0.12.0
golang.org/x/term v0.30.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.9.0
mvdan.cc/sh/v3 v3.11.0
)
require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // 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-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/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.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/tools v0.27.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

182
go.sum
View File

@@ -1,39 +1,92 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
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/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/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/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/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/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.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
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.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
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.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.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.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
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-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/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -47,40 +100,101 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
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/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/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.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.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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
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/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.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
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/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.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
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.9.0 h1:it14fyjCdQUk4jf/aYxLO3FG8jFarR9GzMCtnlvvD7c=
mvdan.cc/sh/v3 v3.9.0/go.mod h1:cdBk8bgoiBI7lSZqK5JhUuq7OB64VQ7fgm85xelw3Nk=
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=

View File

@@ -128,18 +128,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 {

38
init.go
View File

@@ -1,15 +1,13 @@
package task
import (
"fmt"
"io"
"os"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
)
const defaultTaskfile = `# https://taskfile.dev
const DefaultTaskfile = `# https://taskfile.dev
version: '3'
@@ -23,19 +21,31 @@ tasks:
silent: true
`
const defaultTaskfileName = "Taskfile.yml"
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

29
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/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

@@ -2,7 +2,6 @@ package execext
import (
"context"
"errors"
"fmt"
"io"
"os"
@@ -14,6 +13,8 @@ import (
"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

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,
)
}

View File

@@ -0,0 +1,62 @@
package experiments
import (
"fmt"
"slices"
"strconv"
)
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, allowedValues ...int) Experiment {
value := experimentConfig.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,75 @@
package experiments_test
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/experiments"
)
func TestNew(t *testing.T) {
const (
exampleExperiment = "EXAMPLE"
exampleExperimentEnv = "TASK_X_EXAMPLE"
)
tests := []struct {
name string
allowedValues []int
value int
wantEnabled bool
wantActive bool
wantValid error
}{
{
name: `[] allowed, value=""`,
wantEnabled: false,
wantActive: false,
},
{
name: `[] allowed, value="1"`,
value: 1,
wantEnabled: false,
wantActive: false,
wantValid: &experiments.InactiveError{
Name: exampleExperiment,
},
},
{
name: `[1] allowed, value=""`,
allowedValues: []int{1},
wantEnabled: false,
wantActive: true,
},
{
name: `[1] allowed, value="1"`,
allowedValues: []int{1},
value: 1,
wantEnabled: true,
wantActive: true,
},
{
name: `[1] allowed, value="2"`,
allowedValues: []int{1},
value: 2,
wantEnabled: false,
wantActive: true,
wantValid: &experiments.InvalidValueError{
Name: exampleExperiment,
AllowedValues: []int{1},
Value: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.value))
x := experiments.New(exampleExperiment, 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())
})
}
}

View File

@@ -2,28 +2,28 @@ package experiments
import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/Ladicle/tabwriter"
"github.com/Masterminds/semver/v3"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
"github.com/go-task/task/v3/internal/logger"
"gopkg.in/yaml.v3"
)
const envPrefix = "TASK_X_"
type Experiment struct {
Name string
Enabled bool
Value string
var defaultConfigFilenames = []string{
".taskrc.yml",
".taskrc.yaml",
}
type experimentConfigFile struct {
Experiments map[string]int `yaml:"experiments"`
Version *semver.Version
}
// A list of experiments.
var (
GentleForce Experiment
RemoteTaskfiles Experiment
@@ -32,32 +32,35 @@ var (
EnvPrecedence Experiment
)
// An internal list of all the initialized experiments used for iterating.
var (
xList []Experiment
experimentConfig experimentConfigFile
)
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")
experimentConfig = readConfig()
GentleForce = New("GENTLE_FORCE", 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
AnyVariables = New("ANY_VARIABLES")
MapVariables = New("MAP_VARIABLES", 1, 2)
EnvPrecedence = New("ENV_PRECEDENCE", 1)
}
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,
// 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 (x Experiment) String() string {
if x.Enabled {
return fmt.Sprintf("on (%s)", x.Value)
}
return "off"
func List() []Experiment {
return xList
}
func getEnv(xName string) string {
@@ -65,7 +68,7 @@ func getEnv(xName string) string {
return os.Getenv(envName)
}
func getEnvFilePath() string {
func getFilePath(filename string) 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
@@ -76,18 +79,18 @@ func getEnvFilePath() string {
_ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory.
if dir != "" {
return filepath.Join(dir, ".env")
return filepath.Join(dir, filename)
}
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
if taskfile != "" {
return filepath.Join(filepath.Dir(taskfile), ".env")
return filepath.Join(filepath.Dir(taskfile), filename)
}
// Otherwise just use the current working directory.
return ".env"
return filename
}
func readDotEnv() {
env, _ := godotenv.Read(getEnvFilePath())
env, _ := godotenv.Read(getFilePath(".env"))
// If the env var is an experiment, set it.
for key, value := range env {
if strings.HasPrefix(key, envPrefix) {
@@ -96,17 +99,26 @@ func readDotEnv() {
}
}
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 readConfig() experimentConfigFile {
var cfg experimentConfigFile
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()
var content []byte
var err error
for _, filename := range defaultConfigFilenames {
path := getFilePath(filename)
content, err = os.ReadFile(path)
if err == nil {
break
}
}
if err != nil {
return experimentConfigFile{}
}
if err := yaml.Unmarshal(content, &cfg); err != nil {
return experimentConfigFile{}
}
return cfg
}

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

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

View File

@@ -26,6 +26,8 @@ import (
// | false | true | false |
// | false | false | false |
func TestIsTaskUpToDate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
task *ast.Task
@@ -150,6 +152,8 @@ func TestIsTaskUpToDate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockStatusChecker := mocks.NewStatusCheckable(t)
if tt.setupMockStatusChecker != nil {
tt.setupMockStatusChecker(mockStatusChecker)

View File

@@ -1,13 +1,16 @@
package flags
import (
"errors"
"cmp"
"log"
"os"
"strconv"
"time"
"github.com/spf13/pflag"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -77,7 +80,10 @@ func init() {
log.Print(usage)
pflag.PrintDefaults()
}
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil {
offline = false
}
pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
@@ -104,13 +110,13 @@ func init() {
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.")
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
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 {
@@ -118,9 +124,9 @@ 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", false, "Forces Task to only use local or cached Taskfiles.")
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.")
}

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/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"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,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,39 @@
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
// 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 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
})
}
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,78 @@ 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)
})
}
}

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"
@@ -17,8 +18,9 @@ var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"numCPU": func() int { return runtime.NumCPU() },
"catLines": func(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
@@ -59,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
},
@@ -83,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

@@ -140,11 +140,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

@@ -12,13 +12,15 @@ var (
func init() {
info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" {
if !ok || info.Main.Version == "(devel)" || info.Main.Version == "" {
version = "unknown"
} else {
if version == "" {
version = info.Main.Version
}
sum = info.Main.Sum
if sum == "" {
sum = info.Main.Sum
}
}
}

2
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.39.1",
"version": "3.42.0",
"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

@@ -2,8 +2,8 @@ package task
import (
"context"
"errors"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger"

View File

@@ -1,29 +1,30 @@
package task
import (
"slices"
"github.com/go-task/task/v3/errors"
"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 missingVars []errors.MissingVar
for _, requiredVar := range t.Requires.Vars {
if !vars.Exists(requiredVar) {
missingVars = append(missingVars, requiredVar)
_, 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,
}
@@ -31,3 +32,33 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
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.TaskNotAllowedVarsError{
TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars,
}
}
return nil
}

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,14 +64,21 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
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.WithTimeout(e.Timeout),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
graph, err := reader.Read()
if err != nil {
@@ -92,12 +99,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 +113,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.Expand(tempDir)
if err != nil {
return err
}
@@ -128,14 +133,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.Expand(remoteDir)
if err != nil {
return err
}
@@ -191,7 +197,7 @@ func (e *Executor) setupCompiler() error {
}
}
e.Compiler = &compiler.Compiler{
e.Compiler = &Compiler{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
UserWorkingDir: e.UserWorkingDir,
@@ -207,17 +213,21 @@ func (e *Executor) readDotEnvFiles() error {
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 +245,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 +256,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

123
task.go
View File

@@ -11,10 +11,7 @@ import (
"sync/atomic"
"time"
"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"
@@ -28,6 +25,7 @@ import (
"github.com/sajari/fuzzy"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
)
const (
@@ -70,12 +68,13 @@ type Executor struct {
Stdout io.Writer
Stderr io.Writer
Logger *logger.Logger
Compiler *compiler.Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.TaskSorter
UserWorkingDir string
Logger *logger.Logger
Compiler *Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.Sorter
UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model
@@ -86,8 +85,15 @@ type Executor struct {
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 +155,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 +172,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 +182,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 +217,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
@@ -235,13 +246,15 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
}
}
if t.Prompt != "" && !e.Dry {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
for _, p := range t.Prompt {
if p != "" && !e.Dry {
if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
}
@@ -309,7 +322,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
}
@@ -320,7 +333,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()
@@ -330,7 +343,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitC
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.FastGetVariables(origTask, call)
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars}
extra := map[string]any{}
@@ -345,7 +358,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitC
}
}
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 {
@@ -353,7 +366,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
}
@@ -381,7 +394,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,
@@ -393,7 +406,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 {
@@ -439,17 +452,44 @@ 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 now, we can just nil check the task before each loop
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)
matchingTasks := e.FindMatchingTasks(call)
switch len(matchingTasks) {
case 0: // Carry on
case 1:
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
@@ -467,7 +507,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
// 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
@@ -503,8 +543,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) {
@@ -519,7 +564,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
}
@@ -533,12 +578,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

29
taskfile/ast/prompt.go Normal file
View File

@@ -0,0 +1,29 @@
package ast
import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
type Prompt []string
func (p *Prompt) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
*p = []string{str}
return nil
case yaml.SequenceNode:
var list []string
if err := node.Decode(&list); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
*p = list
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("prompt")
}

View File

@@ -1,10 +1,15 @@
package ast
import "github.com/go-task/task/v3/internal/deepcopy"
import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Requires represents a set of required variables necessary for a task to run
type Requires struct {
Vars []string
Vars []*VarsWithValidation
}
func (r *Requires) DeepCopy() *Requires {
@@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
Vars: deepcopy.Slice(r.Vars),
}
}
type VarsWithValidation struct {
Name string
Enum []string
}
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
if v == nil {
return nil
}
return &VarsWithValidation{
Name: v.Name,
Enum: v.Enum,
}
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *VarsWithValidation) 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)
}
v.Name = cmd
v.Enum = nil
return nil
case yaml.MappingNode:
var vv struct {
Name string
Enum []string
}
if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = vv.Name
v.Enum = vv.Enum
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
}

View File

@@ -18,7 +18,7 @@ type Task struct {
Deps []*Dep
Label string
Desc string
Prompt string
Prompt Prompt
Summary string
Requires *Requires
Aliases []string
@@ -115,7 +115,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Deps []*Dep
Label string
Desc string
Prompt string
Prompt Prompt
Summary string
Aliases []string
Sources []*Glob

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
}

View File

@@ -7,11 +7,12 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/taskfile/ast"
)
func TestCmdParse(t *testing.T) {
t.Parallel()
const (
yamlCmd = `echo "a string command"`
yamlDep = `"task-name"`
@@ -38,15 +39,21 @@ vars:
yamlTaskCall,
&ast.Cmd{},
&ast.Cmd{
Task: "another-task", Vars: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"PARAM1": {Value: "VALUE1"},
"PARAM2": {Value: "VALUE2"},
Task: "another-task",
Vars: ast.NewVars(
&ast.VarElement{
Key: "PARAM1",
Value: ast.Var{
Value: "VALUE1",
},
[]string{"PARAM1", "PARAM2"},
),
},
},
&ast.VarElement{
Key: "PARAM2",
Value: ast.Var{
Value: "VALUE2",
},
},
),
},
},
{
@@ -58,14 +65,15 @@ vars:
yamlDeferredCall,
&ast.Cmd{},
&ast.Cmd{
Task: "some_task", Vars: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"PARAM1": {Value: "var"},
Task: "some_task",
Vars: ast.NewVars(
&ast.VarElement{
Key: "PARAM1",
Value: ast.Var{
Value: "var",
},
[]string{"PARAM1"},
),
},
},
),
Defer: true,
},
},
@@ -78,15 +86,21 @@ vars:
yamlTaskCall,
&ast.Dep{},
&ast.Dep{
Task: "another-task", Vars: &ast.Vars{
OrderedMap: omap.FromMapWithOrder(
map[string]ast.Var{
"PARAM1": {Value: "VALUE1"},
"PARAM2": {Value: "VALUE2"},
Task: "another-task",
Vars: ast.NewVars(
&ast.VarElement{
Key: "PARAM1",
Value: ast.Var{
Value: "VALUE1",
},
[]string{"PARAM1", "PARAM2"},
),
},
},
&ast.VarElement{
Key: "PARAM2",
Value: ast.Var{
Value: "VALUE2",
},
},
),
},
},
}

View File

@@ -2,53 +2,126 @@ package ast
import (
"fmt"
"iter"
"slices"
"strings"
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/internal/sort"
)
// Tasks represents a group of tasks
type Tasks struct {
omap.OrderedMap[string, *Task]
type (
// Tasks is an ordered map of task names to Tasks.
Tasks struct {
om *orderedmap.OrderedMap[string, *Task]
mutex sync.RWMutex
}
// A TaskElement is a key-value pair that is used for initializing a Tasks
// structure.
TaskElement orderedmap.Element[string, *Task]
)
// NewTasks creates a new instance of Tasks and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewTasks(els ...*TaskElement) *Tasks {
tasks := &Tasks{
om: orderedmap.NewOrderedMap[string, *Task](),
}
for _, el := range els {
tasks.Set(el.Key, el.Value)
}
return tasks
}
type MatchingTask struct {
Task *Task
Wildcards []string
// Len returns the number of variables in the Tasks map.
func (tasks *Tasks) Len() int {
if tasks == nil || tasks.om == nil {
return 0
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Len()
}
func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
// Get returns the value the the task with the provided key and a boolean that
// indicates if the value was found or not. If the value is not found, the
// returned task is a zero value and the bool is false.
func (tasks *Tasks) Get(key string) (*Task, bool) {
if tasks == nil || tasks.om == nil {
return &Task{}, false
}
var task *Task
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task = t.OrderedMap.Get(call.Task); task != nil {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Get(key)
}
// Set sets the value of the task with the provided key to the provided value.
// If the task already exists, its value is updated. If the task does not exist,
// it is created.
func (tasks *Tasks) Set(key string, value *Task) bool {
if tasks == nil {
tasks = NewTasks()
}
// Attempt a wildcard match
// For now, we can just nil check the task before each loop
_ = t.Range(func(key string, value *Task) error {
if match, wildcards := value.WildcardMatch(call.Task); match {
matchingTasks = append(matchingTasks, &MatchingTask{
Task: value,
Wildcards: wildcards,
})
if tasks.om == nil {
tasks.om = orderedmap.NewOrderedMap[string, *Task]()
}
defer tasks.mutex.Unlock()
tasks.mutex.Lock()
return tasks.om.Set(key, value)
}
// All returns an iterator that loops over all task key-value pairs in the order
// specified by the sorter.
func (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] {
if t == nil || t.om == nil {
return func(yield func(string, *Task) bool) {}
}
if sorter == nil {
return t.om.AllFromFront()
}
return func(yield func(string, *Task) bool) {
for _, key := range sorter(slices.Collect(t.om.Keys()), nil) {
el := t.om.GetElement(key)
if !yield(el.Key, el.Value) {
return
}
}
return nil
})
return matchingTasks
}
}
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) error {
err := t2.Range(func(name string, v *Task) error {
// Keys returns an iterator that loops over all task keys in the order specified
// by the sorter.
func (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] {
return func(yield func(string) bool) {
for k := range t.All(sorter) {
if !yield(k) {
return
}
}
}
}
// Values returns an iterator that loops over all task values in the order
// specified by the sorter.
func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] {
return func(yield func(*Task) bool) {
for _, v := range t.All(sorter) {
if !yield(v) {
return
}
}
}
}
func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
defer t2.mutex.RUnlock()
t2.mutex.RLock()
for name, v := range t2.All(nil) {
// We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged.
task := v.DeepCopy()
@@ -56,6 +129,12 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) e
// taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal)
taskName := name
// if the task is in the exclude list, don't add it to the merged taskfile
if slices.Contains(include.Excludes, name) {
continue
}
if !include.Flatten {
// Add namespaces to task dependencies
for _, dep := range task.Deps {
@@ -94,13 +173,13 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) e
if include.AdvancedImport {
task.Dir = filepathext.SmartJoin(include.Dir, task.Dir)
if task.IncludeVars == nil {
task.IncludeVars = &Vars{}
task.IncludeVars = NewVars()
}
task.IncludeVars.Merge(include.Vars, nil)
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
}
if t1.Get(taskName) != nil {
if _, ok := t1.Get(taskName); ok {
return &errors.TaskNameFlattenConflictError{
TaskName: taskName,
Include: include.Namespace,
@@ -108,56 +187,55 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) e
}
// Add the task to the merged taskfile
t1.Set(taskName, task)
return nil
})
// If the included Taskfile has a default task, being not flattened and the parent namespace has
// no task with a matching name, we can add an alias so that the user can
// run the included Taskfile's default task without specifying its full
// name. If the parent namespace has aliases, we add another alias for each
// of them.
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil && !include.Flatten {
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace)
t1.Get(defaultTaskName).Aliases = slices.Concat(t1.Get(defaultTaskName).Aliases, include.Aliases)
}
return err
// If the included Taskfile has a default task, is not flattened and the
// parent namespace has no task with a matching name, we can add an alias so
// that the user can run the included Taskfile's default task without
// specifying its full name. If the parent namespace has aliases, we add
// another alias for each of them.
_, t2DefaultExists := t2.Get("default")
_, t1NamespaceExists := t1.Get(include.Namespace)
if t2DefaultExists && !t1NamespaceExists && !include.Flatten {
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
t1DefaultTask, ok := t1.Get(defaultTaskName)
if ok {
t1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace)
t1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases)
}
}
return nil
}
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
if t == nil || t.om == nil {
*t = *NewTasks()
}
switch node.Kind {
case yaml.MappingNode:
tasks := omap.New[string, *Task]()
if err := node.Decode(&tasks); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// 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]
// nolint: errcheck
tasks.Range(func(name string, task *Task) error {
// Set the task's name
if task == nil {
task = &Task{
Task: name,
}
// Decode the value node into a Task struct
var v Task
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
task.Task = name
// Set the task's location
for _, keys := range node.Content {
if keys.Value == name {
task.Location = &Location{
Line: keys.Line,
Column: keys.Column,
}
}
// Set the task name and location
v.Task = keyNode.Value
v.Location = &Location{
Line: keyNode.Line,
Column: keyNode.Column,
}
tasks.Set(name, task)
return nil
})
*t = Tasks{
OrderedMap: tasks,
// Add the task to the ordered map
t.Set(keyNode.Value, &v)
}
return nil
}

View File

@@ -7,90 +7,22 @@ import (
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/omap"
)
// Vars is a string[string] variables map.
type Vars struct {
omap.OrderedMap[string, Var]
}
// ToCacheMap converts Vars to a map containing only the static
// variables
func (vs *Vars) ToCacheMap() (m map[string]any) {
m = make(map[string]any, vs.Len())
_ = vs.Range(func(k string, v Var) error {
if v.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
return nil
}
if v.Live != nil {
m[k] = v.Live
} else {
m[k] = v.Value
}
return nil
})
return
}
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
func (vs *Vars) Range(f func(k string, v Var) error) error {
if vs == nil {
return nil
}
return vs.OrderedMap.Range(f)
}
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors
func (vs *Vars) Merge(other *Vars, include *Include) {
if vs == nil || other == nil {
return
}
_ = other.Range(func(key string, value Var) error {
if include != nil && include.AdvancedImport {
value.Dir = include.Dir
}
vs.Set(key, value)
return nil
})
}
// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors
func (vs *Vars) Len() int {
if vs == nil {
return 0
}
return vs.OrderedMap.Len()
}
// DeepCopy creates a new instance of Vars and copies
// data by value from the source struct.
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil
}
return &Vars{
OrderedMap: vs.OrderedMap.DeepCopy(),
}
}
// Var represents either a static or dynamic variable.
type Var struct {
Value any
Live any
Sh string
Sh *string
Ref string
Dir string
}
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Enabled {
if experiments.MapVariables.Enabled() {
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables
if experiments.MapVariables.Value == "1" {
if experiments.MapVariables.Value == 1 {
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -98,7 +30,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
// If the value is a string and it starts with $, then it's a shell command
if str, ok := value.(string); ok {
if str, ok = strings.CutPrefix(str, "$"); ok {
v.Sh = str
v.Sh = &str
return nil
}
if str, ok = strings.CutPrefix(str, "#"); ok {
@@ -111,14 +43,14 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
}
// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
if experiments.MapVariables.Value == "2" {
if experiments.MapVariables.Value == 2 {
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
switch key {
case "sh", "ref", "map":
var m struct {
Sh string
Sh *string
Ref string
Map any
}
@@ -150,7 +82,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch key {
case "sh", "ref":
var m struct {
Sh string
Sh *string
Ref string
}
if err := node.Decode(&m); err != nil {

174
taskfile/ast/vars.go Normal file
View File

@@ -0,0 +1,174 @@
package ast
import (
"iter"
"sync"
"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 (
// Vars is an ordered map of variable names to values.
Vars struct {
om *orderedmap.OrderedMap[string, Var]
mutex sync.RWMutex
}
// A VarElement is a key-value pair that is used for initializing a Vars
// structure.
VarElement orderedmap.Element[string, Var]
)
// NewVars creates a new instance of Vars and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewVars(els ...*VarElement) *Vars {
vars := &Vars{
om: orderedmap.NewOrderedMap[string, Var](),
}
for _, el := range els {
vars.Set(el.Key, el.Value)
}
return vars
}
// Len returns the number of variables in the Vars map.
func (vars *Vars) Len() int {
if vars == nil || vars.om == nil {
return 0
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Len()
}
// Get returns the value the the variable with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned variable is a zero value and the bool is false.
func (vars *Vars) Get(key string) (Var, bool) {
if vars == nil || vars.om == nil {
return Var{}, false
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Get(key)
}
// Set sets the value of the variable with the provided key to the provided
// value. If the variable already exists, its value is updated. If the variable
// does not exist, it is created.
func (vars *Vars) Set(key string, value Var) bool {
if vars == nil {
vars = NewVars()
}
if vars.om == nil {
vars.om = orderedmap.NewOrderedMap[string, Var]()
}
defer vars.mutex.Unlock()
vars.mutex.Lock()
return vars.om.Set(key, value)
}
// All returns an iterator that loops over all task key-value pairs.
func (vars *Vars) All() iter.Seq2[string, Var] {
if vars == nil || vars.om == nil {
return func(yield func(string, Var) bool) {}
}
return vars.om.AllFromFront()
}
// Keys returns an iterator that loops over all task keys.
func (vars *Vars) Keys() iter.Seq[string] {
if vars == nil || vars.om == nil {
return func(yield func(string) bool) {}
}
return vars.om.Keys()
}
// Values returns an iterator that loops over all task values.
func (vars *Vars) Values() iter.Seq[Var] {
if vars == nil || vars.om == nil {
return func(yield func(Var) bool) {}
}
return vars.om.Values()
}
// ToCacheMap converts Vars to an unordered map containing only the static
// variables
func (vars *Vars) ToCacheMap() (m map[string]any) {
defer vars.mutex.RUnlock()
vars.mutex.RLock()
m = make(map[string]any, vars.Len())
for k, v := range vars.All() {
if v.Sh != nil && *v.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
continue
}
if v.Live != nil {
m[k] = v.Live
} else {
m[k] = v.Value
}
}
return
}
// Merge loops over other and merges it values with the variables in vars. If
// the include parameter is not nil and its it is an advanced import, the
// directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return
}
defer other.mutex.RUnlock()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
if include != nil && include.AdvancedImport {
pair.Value.Dir = include.Dir
}
vars.om.Set(pair.Key, pair.Value)
}
}
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil
}
defer vs.mutex.RUnlock()
vs.mutex.RLock()
return &Vars{
om: deepcopy.OrderedMap(vs.om),
}
}
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if vs == nil || vs.om == nil {
*vs = *NewVars()
}
vs.om = orderedmap.NewOrderedMap[string, Var]()
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]
// Decode the value node into a Task struct
var v Var
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map
vs.Set(keyNode.Value, v)
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars")
}

View File

@@ -1,27 +1,22 @@
package taskfile
import (
"fmt"
"os"
"github.com/joho/godotenv"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, error) {
func Dotenv(vars *ast.Vars, tf *ast.Taskfile, dir string) (*ast.Vars, error) {
if len(tf.Dotenv) == 0 {
return nil, nil
}
vars, err := c.GetTaskfileVariables()
if err != nil {
return nil, err
}
env := &ast.Vars{}
env := ast.NewVars()
cache := &templater.Cache{Vars: vars}
for _, dotEnvPath := range tf.Dotenv {
@@ -37,10 +32,10 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
envs, err := godotenv.Read(dotEnvPath)
if err != nil {
return nil, err
return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, err)
}
for key, value := range envs {
if ok := env.Exists(key); !ok {
if _, ok := env.Get(key); !ok {
env.Set(key, ast.Var{Value: value})
}
}

View File

@@ -7,9 +7,10 @@ import (
"strings"
"time"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/logger"
)
type Node interface {
@@ -24,7 +25,6 @@ type Node interface {
}
func NewRootNode(
l *logger.Logger,
entrypoint string,
dir string,
insecure bool,
@@ -35,11 +35,10 @@ func NewRootNode(
if entrypoint == "-" {
return NewStdinNode(dir)
}
return NewNode(l, entrypoint, dir, insecure, timeout)
return NewNode(entrypoint, dir, insecure, timeout)
}
func NewNode(
l *logger.Logger,
entrypoint string,
dir string,
insecure bool,
@@ -48,24 +47,39 @@ func NewNode(
) (Node, error) {
var node Node
var err error
switch getScheme(entrypoint) {
case "http", "https":
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
default:
// If no other scheme matches, we assume it's a file
node, err = NewFileNode(l, entrypoint, dir, opts...)
scheme, err := getScheme(entrypoint)
if err != nil {
return nil, err
}
if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
switch scheme {
case "git":
node, err = NewGitNode(entrypoint, dir, insecure, opts...)
case "http", "https":
node, err = NewHTTPNode(entrypoint, dir, insecure, timeout, opts...)
default:
node, err = NewFileNode(entrypoint, dir, opts...)
}
if node.Remote() && !experiments.RemoteTaskfiles.Enabled() {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
}
return node, err
}
func getScheme(uri string) string {
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i]
func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri)
if u == nil {
return "", err
}
return ""
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
return "git", nil
}
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil
}
return "", nil
}
func getDefaultDir(entrypoint, dir string) string {

View File

@@ -9,7 +9,6 @@ import (
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
)
// A FileNode is a node that reads a taskfile from the local filesystem.
@@ -18,10 +17,10 @@ type FileNode struct {
Entrypoint string
}
func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error
base := NewBaseNode(dir, opts...)
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, base.dir)
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(entrypoint, base.dir)
if err != nil {
return nil, err
}
@@ -50,10 +49,10 @@ func (node *FileNode) Read(ctx context.Context) ([]byte, error) {
// resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and
// populates them with default values if necessary.
func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (string, string, error) {
func resolveFileNodeEntrypointAndDir(entrypoint, dir string) (string, string, error) {
var err error
if entrypoint != "" {
entrypoint, err = Exists(l, entrypoint)
entrypoint, err = Exists(entrypoint)
if err != nil {
return "", "", err
}
@@ -68,7 +67,7 @@ func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (
return "", "", err
}
}
entrypoint, err = ExistsWalk(l, dir)
entrypoint, err = ExistsWalk(dir)
if err != nil {
return "", "", err
}
@@ -81,6 +80,9 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
return entrypoint, nil
}
path, err := execext.Expand(entrypoint)
if err != nil {

126
taskfile/node_git.go Normal file
View File

@@ -0,0 +1,126 @@
package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"path/filepath"
"strings"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
)
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
rawUrl string
ref string
path string
}
func NewGitNode(
entrypoint string,
dir string,
insecure bool,
opts ...NodeOption,
) (*GitNode, error) {
base := NewBaseNode(dir, opts...)
u, err := giturls.Parse(entrypoint)
if err != nil {
return nil, err
}
basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref")
rawUrl := u.String()
u.RawQuery = ""
u.Path = basePath
if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
}
return &GitNode{
BaseNode: base,
URL: u,
rawUrl: rawUrl,
ref: ref,
path: path,
}, nil
}
func (node *GitNode) Location() string {
return node.rawUrl
}
func (node *GitNode) Remote() bool {
return true
}
func (node *GitNode) Read(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
if err != nil {
return nil, err
}
file, err := fs.Open(node.path)
if err != nil {
return nil, err
}
// Read the entire response body
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return b, nil
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
return resolvedEntrypoint, nil
}
func (node *GitNode) ResolveDir(dir string) (string, error) {
path, err := execext.Expand(dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
}
func (node *GitNode) FilenameAndLastDir() (string, string) {
return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
}

85
taskfile/node_git_test.go Normal file
View File

@@ -0,0 +1,85 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitNode_ssh(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_https(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_httpsWithDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_FilenameAndDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir := node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, ".", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
}

View File

@@ -11,19 +11,17 @@ import (
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
)
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*BaseNode
URL *url.URL
logger *logger.Logger
timeout time.Duration
URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
entrypoint string // stores entrypoint url. used for building graph vertices.
timeout time.Duration
}
func NewHTTPNode(
l *logger.Logger,
entrypoint string,
dir string,
insecure bool,
@@ -40,15 +38,15 @@ func NewHTTPNode(
}
return &HTTPNode{
BaseNode: base,
URL: url,
timeout: timeout,
logger: l,
BaseNode: base,
URL: url,
entrypoint: entrypoint,
timeout: timeout,
}, nil
}
func (node *HTTPNode) Location() string {
return node.URL.String()
return node.entrypoint
}
func (node *HTTPNode) Remote() bool {
@@ -56,7 +54,7 @@ func (node *HTTPNode) Remote() bool {
}
func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout)
url, err := RemoteExists(ctx, node.URL, node.timeout)
if err != nil {
return nil, err
}
@@ -74,7 +72,6 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{
URI: node.URL.String(),
@@ -111,11 +108,15 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
parent := node.Dir()
if node.Parent() != nil {
parent = node.Parent().Dir()
}
return filepathext.SmartJoin(parent, path), nil
}
func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.URL.Path)
dir, filename := filepath.Split(node.entrypoint)
return filepath.Base(dir), filename
}

24
taskfile/node_test.go Normal file
View File

@@ -0,0 +1,24 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestScheme(t *testing.T) {
t.Parallel()
scheme, err := getScheme("https://github.com/foo/bar.git")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("https://github.com/foo/bar.git?ref=v1//taskfile/common.yml")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("git@github.com:foo/bar.git?ref=main//Taskfile.yml")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
scheme, err = getScheme("https://github.com/foo/common.yml")
assert.NoError(t, err)
assert.Equal(t, "https", scheme)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/dominikbraun/graph"
@@ -11,9 +12,8 @@ import (
"gopkg.in/yaml.v3"
"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/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -27,37 +27,114 @@ Continue?`
Continue?`
)
// A Reader will recursively read Taskfiles from a given source using a directed
// acyclic graph (DAG).
type Reader struct {
graph *ast.TaskfileGraph
node Node
insecure bool
download bool
offline bool
timeout time.Duration
tempDir string
logger *logger.Logger
}
type (
// ReaderDebugFunc is a function that is called when the reader wants to
// log debug messages
ReaderDebugFunc func(string)
// ReaderPromptFunc is a function that is called when the reader wants to
// prompt the user in some way
ReaderPromptFunc func(string) error
// ReaderOption is a function that configures a Reader.
ReaderOption func(*Reader)
// A Reader will recursively read Taskfiles from a given source using a directed
// acyclic graph (DAG).
Reader struct {
graph *ast.TaskfileGraph
node Node
insecure bool
download bool
offline bool
timeout time.Duration
tempDir string
debugFunc ReaderDebugFunc
promptFunc ReaderPromptFunc
promptMutex sync.Mutex
}
)
// NewReader constructs a new Taskfile Reader using the given Node and options.
func NewReader(
node Node,
insecure bool,
download bool,
offline bool,
timeout time.Duration,
tempDir string,
logger *logger.Logger,
opts ...ReaderOption,
) *Reader {
return &Reader{
graph: ast.NewTaskfileGraph(),
node: node,
insecure: insecure,
download: download,
offline: offline,
timeout: timeout,
tempDir: tempDir,
logger: logger,
reader := &Reader{
graph: ast.NewTaskfileGraph(),
node: node,
insecure: false,
download: false,
offline: false,
timeout: time.Second * 10,
tempDir: os.TempDir(),
debugFunc: nil,
promptFunc: nil,
promptMutex: sync.Mutex{},
}
for _, opt := range opts {
opt(reader)
}
return reader
}
// WithInsecure enables insecure connections when reading remote taskfiles. By
// default, insecure connections are rejected.
func WithInsecure(insecure bool) ReaderOption {
return func(r *Reader) {
r.insecure = insecure
}
}
// WithDownload forces the reader to download a fresh copy of the taskfile from
// the remote source.
func WithDownload(download bool) ReaderOption {
return func(r *Reader) {
r.download = download
}
}
// WithOffline stops the reader 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) ReaderOption {
return func(r *Reader) {
r.offline = offline
}
}
// WithTimeout sets the timeout for reading remote taskfiles. By default, the
// timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ReaderOption {
return func(r *Reader) {
r.timeout = timeout
}
}
// WithTempDir sets the temporary directory to be used by the reader. By
// default, the reader uses `os.TempDir()`.
func WithTempDir(tempDir string) ReaderOption {
return func(r *Reader) {
r.tempDir = tempDir
}
}
// WithDebugFunc sets the debug function to be used by the reader. If set, this
// function will be called with debug messages. This can be useful if the caller
// wants to log debug messages from the reader. By default, no debug function is
// set and the logs are not written.
func WithDebugFunc(debugFunc ReaderDebugFunc) ReaderOption {
return func(r *Reader) {
r.debugFunc = debugFunc
}
}
// WithPromptFunc sets the prompt function to be used by the reader. If set,
// this function will be called with prompt messages. The function should
// optionally log the message to the user and return nil if the prompt is
// accepted and the execution should continue. Otherwise, it should return an
// error which describes why the the prompt was rejected. This can then be
// caught and used later when calling the Read method. By default, no prompt
// function is set and all prompts are automatically accepted.
func WithPromptFunc(promptFunc ReaderPromptFunc) ReaderOption {
return func(r *Reader) {
r.promptFunc = promptFunc
}
}
@@ -70,6 +147,19 @@ func (r *Reader) Read() (*ast.TaskfileGraph, error) {
return r.graph, nil
}
func (r *Reader) debugf(format string, a ...any) {
if r.debugFunc != nil {
r.debugFunc(fmt.Sprintf(format, a...))
}
}
func (r *Reader) promptf(format string, a ...any) error {
if r.promptFunc != nil {
return r.promptFunc(fmt.Sprintf(format, a...))
}
return nil
}
func (r *Reader) include(node Node) error {
// Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{
@@ -97,8 +187,8 @@ func (r *Reader) include(node Node) error {
var g errgroup.Group
// Loop over each included taskfile
_ = vertex.Taskfile.Includes.Range(func(namespace string, include *ast.Include) error {
vars := compiler.GetEnviron()
for _, include := range vertex.Taskfile.Includes.All() {
vars := env.GetEnviron()
vars.Merge(vertex.Taskfile.Vars, nil)
// Start a goroutine to process each included Taskfile
g.Go(func() error {
@@ -112,6 +202,7 @@ func (r *Reader) include(node Node) error {
Flatten: include.Flatten,
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Excludes: include.Excludes,
Vars: include.Vars,
}
if err := cache.Err(); err != nil {
@@ -128,7 +219,7 @@ func (r *Reader) include(node Node) error {
return err
}
includeNode, err := NewNode(r.logger, entrypoint, include.Dir, r.insecure, r.timeout,
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure, r.timeout,
WithParent(node),
)
if err != nil {
@@ -173,104 +264,29 @@ func (r *Reader) include(node Node) error {
}
return err
})
return nil
})
}
// Wait for all the go routines to finish
return g.Wait()
}
func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
var b []byte
var err error
var cache *Cache
if node.Remote() {
cache, err = NewCache(r.tempDir)
if err != nil {
return nil, err
}
}
// If the file is remote and we're in offline mode, check if we have a cached copy
if node.Remote() && r.offline {
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
} else if err != nil {
return nil, err
}
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
} else {
downloaded := false
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
defer cf()
// Read the file
b, err = node.Read(ctx)
var taskfileNetworkTimeoutError *errors.TaskfileNetworkTimeoutError
// If we timed out then we likely have a network issue
if node.Remote() && errors.As(err, &taskfileNetworkTimeoutError) {
// If a download was requested, then we can't use a cached copy
if r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
}
// Search for any cached copies
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout, CheckedCache: true}
} else if err != nil {
return nil, err
}
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
} else if err != nil {
return nil, err
} else {
downloaded = true
}
// If the node was remote, we need to check the checksum
if node.Remote() && downloaded {
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
// Get the checksums
checksum := checksum(b)
cachedChecksum := cache.readChecksum(node)
var prompt string
if cachedChecksum == "" {
// If the checksum doesn't exist, prompt the user to continue
prompt = fmt.Sprintf(taskfileUntrustedPrompt, node.Location())
} else if checksum != cachedChecksum {
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
}
if prompt != "" {
if err := r.logger.Prompt(logger.Yellow, prompt, "n", "y", "yes"); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
}
// If the hash has changed (or is new)
if checksum != cachedChecksum {
// Store the checksum
if err := cache.writeChecksum(node, checksum); err != nil {
return nil, err
}
// Cache the file
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
if err = cache.write(node, b); err != nil {
return nil, err
}
}
}
b, err := r.loadNodeContent(node)
if err != nil {
return nil, err
}
var tf ast.Taskfile
if err := yaml.Unmarshal(b, &tf); err != nil {
// Decode the taskfile and add the file info the any errors
taskfileInvalidErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) {
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2)
taskfileDecodeErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileDecodeErr) {
snippet := NewSnippet(b,
SnippetWithLine(taskfileDecodeErr.Line),
SnippetWithColumn(taskfileDecodeErr.Column),
SnippetWithPadding(2),
)
return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String())
}
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
}
@@ -282,7 +298,7 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
// Set the taskfile/task's locations
tf.Location = node.Location()
for _, task := range tf.Tasks.Values() {
for task := range tf.Tasks.Values(nil) {
// If the task is not defined, create a new one
if task == nil {
task = &ast.Task{}
@@ -295,3 +311,93 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
return &tf, nil
}
func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
if !node.Remote() {
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
defer cf()
return node.Read(ctx)
}
cache, err := NewCache(r.tempDir)
if err != nil {
return nil, err
}
if r.offline {
// In offline mode try to use cached copy
cached, err := cache.read(node)
if errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
} else if err != nil {
return nil, err
}
r.debugf("task: [%s] Fetched cached copy\n", node.Location())
return cached, nil
}
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
defer cf()
b, err := node.Read(ctx)
if errors.Is(err, &errors.TaskfileNetworkTimeoutError{}) {
// If we timed out then we likely have a network issue
// If a download was requested, then we can't use a cached copy
if r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
}
// Search for any cached copies
cached, err := cache.read(node)
if errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout, CheckedCache: true}
} else if err != nil {
return nil, err
}
r.debugf("task: [%s] Network timeout. Fetched cached copy\n", node.Location())
return cached, nil
} else if err != nil {
return nil, err
}
r.debugf("task: [%s] Fetched remote copy\n", node.Location())
// Get the checksums
checksum := checksum(b)
cachedChecksum := cache.readChecksum(node)
var prompt string
if cachedChecksum == "" {
// If the checksum doesn't exist, prompt the user to continue
prompt = taskfileUntrustedPrompt
} else if checksum != cachedChecksum {
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
prompt = taskfileChangedPrompt
}
if prompt != "" {
if err := func() error {
r.promptMutex.Lock()
defer r.promptMutex.Unlock()
return r.promptf(prompt, node.Location())
}(); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
// Store the checksum
if err := cache.writeChecksum(node, checksum); err != nil {
return nil, err
}
// Cache the file
r.debugf("task: [%s] Caching downloaded file\n", node.Location())
if err = cache.write(node, b); err != nil {
return nil, err
}
}
return b, nil
}

148
taskfile/snippet.go Normal file
View File

@@ -0,0 +1,148 @@
package taskfile
import (
"bytes"
"embed"
"fmt"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color"
)
//go:embed themes/*.xml
var embedded embed.FS
const (
lineIndicator = ">"
columnIndicator = "^"
)
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 (
SnippetOption func(*Snippet)
Snippet struct {
linesRaw []string
linesHighlighted []string
start int
end int
line int
column int
padding int
noIndicators bool
}
)
// NewSnippet creates a new snippet from a byte slice and a line and column
// number. The line and column numbers should be 1-indexed. For example, the
// first character in the file would be 1:1 (line 1, column 1). The padding
// determines the number of lines to include before and after the chosen line.
func NewSnippet(b []byte, opts ...SnippetOption) *Snippet {
snippet := &Snippet{}
for _, opt := range opts {
opt(snippet)
}
// Syntax highlight the input and split it into lines
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.Write(b)
}
linesRaw := strings.Split(string(b), "\n")
linesHighlighted := strings.Split(buf.String(), "\n")
// Work out the start and end lines of the snippet
snippet.start = max(snippet.line-snippet.padding, 1)
snippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1)
snippet.linesRaw = linesRaw[snippet.start-1 : snippet.end]
snippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end]
return snippet
}
func SnippetWithLine(line int) SnippetOption {
return func(snippet *Snippet) {
snippet.line = line
}
}
func SnippetWithColumn(column int) SnippetOption {
return func(snippet *Snippet) {
snippet.column = column
}
}
func SnippetWithPadding(padding int) SnippetOption {
return func(snippet *Snippet) {
snippet.padding = padding
}
}
func SnippetWithNoIndicators() SnippetOption {
return func(snippet *Snippet) {
snippet.noIndicators = true
}
}
func (snippet *Snippet) String() string {
buf := &bytes.Buffer{}
maxLineNumberDigits := digits(snippet.end)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator))
columnSpacer := strings.Repeat(" ", max(snippet.column-1, 0))
// Loop over each line in the snippet
for i, lineHighlighted := range snippet.linesHighlighted {
if i > 0 {
fmt.Fprintln(buf)
}
currentLine := snippet.start + i
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
// If this is a padding line or indicators are disabled, print it as normal
if currentLine != snippet.line || snippet.noIndicators {
fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, lineHighlighted)
continue
}
// Otherwise, print the line with indicators
fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, lineHighlighted)
// Only print the column indicator if the column is in bounds
if snippet.column > 0 && snippet.column <= len(snippet.linesRaw[i]) {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
}
// If there are lines, but no line is selected, print the column indicator under all the lines
if len(snippet.linesHighlighted) > 0 && snippet.line == 0 && snippet.column > 0 {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
return buf.String()
}
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}

289
taskfile/snippet_test.go Normal file
View File

@@ -0,0 +1,289 @@
package taskfile
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
const sample = `version: 3
tasks:
default:
vars:
FOO: foo
BAR: bar
cmds:
- echo "{{.FOO}}"
- echo "{{.BAR}}"
`
func TestNewSnippet(t *testing.T) {
t.Parallel()
tests := []struct {
name string
b []byte
opts []SnippetOption
want *Snippet
}{
{
name: "first line, first column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: &Snippet{
linesRaw: []string{
"version: 3",
},
linesHighlighted: []string{
"\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
start: 1,
end: 1,
line: 1,
column: 1,
padding: 0,
},
},
{
name: "first line, first column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: &Snippet{
linesRaw: []string{
"version: 3",
"",
"tasks:",
},
linesHighlighted: []string{
"\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
"\x1b[1m\x1b[30m\x1b[0m",
"\x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
start: 1,
end: 3,
line: 1,
column: 1,
padding: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := NewSnippet(tt.b, tt.opts...)
require.Equal(t, tt.want, got)
})
}
}
func TestSnippetString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
b []byte
opts []SnippetOption
want string
}{
{
name: "empty",
b: []byte{},
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: "",
},
{
name: "0th line, 0th column (no indicators)",
b: []byte(sample),
want: "",
},
{
name: "1st line, 0th column (line indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "0th line, 1st column (column indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithColumn(1),
},
want: "",
},
{
name: "0th line, 1st column, padding=2 (column indicator only)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 10th column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(10),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "1st line, 1st column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "1st line, 10th column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(1),
SnippetWithColumn(10),
SnippetWithPadding(2),
},
want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "5th line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(1),
},
want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
},
want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(2),
},
want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "5th line, 5th column, padding=2, no indicators",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(2),
SnippetWithNoIndicators(),
},
want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "10th line, 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(1),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "10th line, 23rd column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(23),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "10th line, 24th column (out of bounds)",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(24),
},
want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "10th line, 23rd column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(10),
SnippetWithColumn(23),
SnippetWithPadding(2),
},
want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^",
},
{
name: "5th line, 5th column, padding=100",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(5),
SnippetWithColumn(5),
SnippetWithPadding(100),
},
want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
{
name: "11th line (out of bounds), 1st column",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(11),
SnippetWithColumn(1),
},
want: "",
},
{
name: "11th line (out of bounds), 1st column, padding=2",
b: []byte(sample),
opts: []SnippetOption{
SnippetWithLine(11),
SnippetWithColumn(1),
SnippetWithPadding(2),
},
want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
snippet := NewSnippet(tt.b, tt.opts...)
got := snippet.String()
if strings.Contains(got, "\t") {
t.Fatalf("tab character found in snippet - check the sample string")
}
require.Equal(t, tt.want, got)
})
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sysinfo"
)
@@ -41,15 +40,15 @@ var (
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) {
func RemoteExists(ctx context.Context, u *url.URL, timeout time.Duration) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequest("HEAD", u.String(), nil)
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
}
// Request the given URL
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: u.String(), Timeout: timeout}
@@ -89,7 +88,6 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout tim
// If the request was successful, return the URL
if resp.StatusCode == http.StatusOK {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile)
return alt, nil
}
}
@@ -102,7 +100,7 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout tim
// given path with any of the default Taskfile files names. If any of these
// match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func Exists(l *logger.Logger, path string) (string, error) {
func Exists(path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", err
@@ -117,7 +115,6 @@ func Exists(l *logger.Logger, path string) (string, error) {
for _, taskfile := range defaultTaskfiles {
alt := filepathext.SmartJoin(path, taskfile)
if _, err := os.Stat(alt); err == nil {
l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile)
return filepath.Abs(alt)
}
}
@@ -130,14 +127,14 @@ func Exists(l *logger.Logger, path string) (string, error) {
// calling the exists function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
func ExistsWalk(l *logger.Logger, path string) (string, error) {
func ExistsWalk(path string) (string, error) {
origPath := path
owner, err := sysinfo.Owner(path)
if err != nil {
return "", err
}
for {
fpath, err := Exists(l, path)
fpath, err := Exists(path)
if err == nil {
return fpath, nil
}

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