mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
389 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
209c88c341 | ||
|
|
bd94f9f607 | ||
|
|
9f6b78ec84 | ||
|
|
fbde227167 | ||
|
|
fc06e92a87 | ||
|
|
a0cab3f5ec | ||
|
|
bb4c254211 | ||
|
|
57bf348829 | ||
|
|
092b9b6391 | ||
|
|
cd8c831204 | ||
|
|
0d03f4f266 | ||
|
|
b8bf298c84 | ||
|
|
9a91c4cb21 | ||
|
|
2921450bf7 | ||
|
|
dffa355cad | ||
|
|
48039be12c | ||
|
|
43cb64e6cc | ||
|
|
25a7b5936f | ||
|
|
4ae3071845 | ||
|
|
242523c797 | ||
|
|
0fdb5e8665 | ||
|
|
534dfa089c | ||
|
|
51a3bcaacd | ||
|
|
6289fcf34c | ||
|
|
2959737d7d | ||
|
|
a3047d3cd8 | ||
|
|
725600f220 | ||
|
|
fd83414074 | ||
|
|
6c645a33f7 | ||
|
|
9d969e5971 | ||
|
|
8b382a3bae | ||
|
|
a34892ad94 | ||
|
|
e55bb29554 | ||
|
|
1168ef32df | ||
|
|
245d7f747f | ||
|
|
b216ae885c | ||
|
|
61cb15ad01 | ||
|
|
04579c0c44 | ||
|
|
39462cbfde | ||
|
|
72dfec68b0 | ||
|
|
f89c12ddf0 | ||
|
|
c903d07332 | ||
|
|
138b9a5a4f | ||
|
|
1e2121a99f | ||
|
|
9495fb2b1c | ||
|
|
1fda55910e | ||
|
|
e6c808c02b | ||
|
|
0fc26a43a9 | ||
|
|
c0b4c19443 | ||
|
|
1a8df44e9e | ||
|
|
82ad1de8d0 | ||
|
|
d59c795502 | ||
|
|
504cb94e8b | ||
|
|
e7606635fe | ||
|
|
9a05ceaa80 | ||
|
|
083654d8c9 | ||
|
|
79c93fb42b | ||
|
|
64fc538a16 | ||
|
|
4da081e5c3 | ||
|
|
4bdfe5ce3b | ||
|
|
26ef693417 | ||
|
|
952f32d388 | ||
|
|
e72c35f79f | ||
|
|
72991d4f04 | ||
|
|
6f965e3043 | ||
|
|
1c6d686356 | ||
|
|
dac5aa1954 | ||
|
|
303bd6ccb2 | ||
|
|
f736cfaaf1 | ||
|
|
53f97889bc | ||
|
|
fe2da74ea3 | ||
|
|
64fb66895b | ||
|
|
d2bd834c81 | ||
|
|
8a43ca5d8f | ||
|
|
a10a9faabf | ||
|
|
3d3ed0e403 | ||
|
|
47dc87a2c9 | ||
|
|
3b0a746f85 | ||
|
|
281edfe5b3 | ||
|
|
7289ffce0b | ||
|
|
61e1af50ff | ||
|
|
715a143735 | ||
|
|
a0b1605634 | ||
|
|
69fc13bd13 | ||
|
|
b42a52ba77 | ||
|
|
cb812476b3 | ||
|
|
b09c6870fe | ||
|
|
86e4a3aac7 | ||
|
|
7782bc92ae | ||
|
|
9cc2d65091 | ||
|
|
b932e539d9 | ||
|
|
be45eb04d9 | ||
|
|
6b878980dc | ||
|
|
cd910abd45 | ||
|
|
6e524bb2fa | ||
|
|
b4c8f5a0fe | ||
|
|
09f85844ba | ||
|
|
d54d2ccabc | ||
|
|
cf81ab3112 | ||
|
|
aaa7b7772d | ||
|
|
71eb8cdeea | ||
|
|
68ce8b1d84 | ||
|
|
5323990c72 | ||
|
|
ec4e68d601 | ||
|
|
bb5b045293 | ||
|
|
89f29cb75b | ||
|
|
da4ce5b0a5 | ||
|
|
fb68a5f79a | ||
|
|
f40f389cb4 | ||
|
|
a459eeaabb | ||
|
|
84f02a822f | ||
|
|
55d1aa260d | ||
|
|
e7084cdf26 | ||
|
|
ca55e9b621 | ||
|
|
6528b36caa | ||
|
|
f8736c5f77 | ||
|
|
6896accf86 | ||
|
|
c12ed49acb | ||
|
|
d1bfd3e9f7 | ||
|
|
fc17343fcc | ||
|
|
d3e9be1520 | ||
|
|
d850d03c96 | ||
|
|
0058f18676 | ||
|
|
b3c4007756 | ||
|
|
9e8fd54be9 | ||
|
|
a33544101a | ||
|
|
1c35358fcc | ||
|
|
13daa6dc35 | ||
|
|
20c1ffe098 | ||
|
|
bd8ccb8d03 | ||
|
|
8162b05f59 | ||
|
|
68d5095761 | ||
|
|
6cb0a5a2f2 | ||
|
|
08056924e0 | ||
|
|
39706105e1 | ||
|
|
bf4e7960cb | ||
|
|
3d36616e9e | ||
|
|
3976e8372a | ||
|
|
c2123dc016 | ||
|
|
0a6cd1ee42 | ||
|
|
7169bf6434 | ||
|
|
84cd4dfdad | ||
|
|
672b39413f | ||
|
|
7eebf6e704 | ||
|
|
4834ac743c | ||
|
|
c5afffb551 | ||
|
|
1ae3bf0b25 | ||
|
|
a84f09d45f | ||
|
|
f47f237093 | ||
|
|
04df108fb5 | ||
|
|
8885d9e4f7 | ||
|
|
a60c2ec3f8 | ||
|
|
f789c57624 | ||
|
|
7416b7d77e | ||
|
|
c1ab661cf2 | ||
|
|
768dca053b | ||
|
|
e65159f613 | ||
|
|
789a7ea950 | ||
|
|
b11da93c78 | ||
|
|
8c720b03aa | ||
|
|
8c8b1b5f3b | ||
|
|
38b42d0fb1 | ||
|
|
669bf33619 | ||
|
|
6f0f38b8d9 | ||
|
|
a9de239e38 | ||
|
|
f0414f162d | ||
|
|
a24f4958cd | ||
|
|
55790be6ad | ||
|
|
88fdbd13cf | ||
|
|
566ac29932 | ||
|
|
ffef3ed1a6 | ||
|
|
2a60842707 | ||
|
|
41bd866813 | ||
|
|
01bc0a0529 | ||
|
|
a6a9792b7e | ||
|
|
ce032dc46b | ||
|
|
f07f4c85b2 | ||
|
|
cd81d94e18 | ||
|
|
1939f83ffe | ||
|
|
2a92b70bc2 | ||
|
|
4736bc2734 | ||
|
|
180fcef364 | ||
|
|
f6baa5942e | ||
|
|
d54b0d6a2a | ||
|
|
03b242d4c3 | ||
|
|
60e28ecdcc | ||
|
|
dd8daa68cd | ||
|
|
55617e062f | ||
|
|
c6f1b3ae4f | ||
|
|
cb14a4f3a1 | ||
|
|
0d5f2b5dab | ||
|
|
89caf1e049 | ||
|
|
7f7e8306da | ||
|
|
1f2eecda9e | ||
|
|
60c959c75c | ||
|
|
a771e91ff3 | ||
|
|
532644d7f8 | ||
|
|
b68f4067d9 | ||
|
|
c544b0058d | ||
|
|
d1360ee72a | ||
|
|
076aff1f8e | ||
|
|
ffeb3bcc3f | ||
|
|
8181352d54 | ||
|
|
23fd7e782c | ||
|
|
6604b9a8cc | ||
|
|
6ee1053c96 | ||
|
|
8eaf83599e | ||
|
|
cd086228b2 | ||
|
|
1b8b399c7e | ||
|
|
8426f84b18 | ||
|
|
14bbb324e5 | ||
|
|
b9d202c491 | ||
|
|
c23c46e326 | ||
|
|
a266fba93e | ||
|
|
fb631902ce | ||
|
|
b14125bacd | ||
|
|
3c5782f4a4 | ||
|
|
60c8ee0ce6 | ||
|
|
cdaf69e03d | ||
|
|
d6234af49a | ||
|
|
a31f2cf4a8 | ||
|
|
0dd6f78855 | ||
|
|
6f80777faf | ||
|
|
8558e0c48a | ||
|
|
461714a899 | ||
|
|
8a35033abc | ||
|
|
daf39a04bf | ||
|
|
25f9299d0a | ||
|
|
4d15a8be8f | ||
|
|
cbde4c33f8 | ||
|
|
cdb6a3f70a | ||
|
|
fb27318601 | ||
|
|
35ea4e0460 | ||
|
|
2b4d9bfba7 | ||
|
|
ce96447468 | ||
|
|
e7a6de64cb | ||
|
|
ff8c913ce7 | ||
|
|
0e23404d23 | ||
|
|
65a64a01ee | ||
|
|
f6ec7444d5 | ||
|
|
6ce798e16c | ||
|
|
be81885835 | ||
|
|
69ac06170a | ||
|
|
c995fe6d11 | ||
|
|
9009124192 | ||
|
|
80f96d67da | ||
|
|
002b8c929a | ||
|
|
b5b1524d3a | ||
|
|
3aee0a0519 | ||
|
|
23df1f0c61 | ||
|
|
edbb83f6de | ||
|
|
c903d5c6f4 | ||
|
|
88c4ba1740 | ||
|
|
7d4c52546a | ||
|
|
f5121de468 | ||
|
|
b5d573fbd9 | ||
|
|
888de0f8ef | ||
|
|
09b11d343b | ||
|
|
a2390d0dca | ||
|
|
0f633091eb | ||
|
|
6b16c532c2 | ||
|
|
69f5714e45 | ||
|
|
b3e4cfcf48 | ||
|
|
65a71e5df3 | ||
|
|
bad2c8fcc1 | ||
|
|
97f41b710e | ||
|
|
240047152d | ||
|
|
24a830e384 | ||
|
|
fe9f489702 | ||
|
|
27de441ed2 | ||
|
|
79f7af2b04 | ||
|
|
b588d49cfb | ||
|
|
45006e2ce0 | ||
|
|
e5d8237053 | ||
|
|
89740ed72a | ||
|
|
c1e14c461b | ||
|
|
dc2eceb634 | ||
|
|
43f3dcea05 | ||
|
|
f27daea5c9 | ||
|
|
49e88e92cf | ||
|
|
da40aabcc7 | ||
|
|
8ce9bdc8c7 | ||
|
|
0409c3c3ba | ||
|
|
fd3532812e | ||
|
|
2965841eb7 | ||
|
|
dbe6e41ac8 | ||
|
|
8f73ced037 | ||
|
|
2a4f93eb41 | ||
|
|
9d8c4ba7e6 | ||
|
|
1bda388925 | ||
|
|
d64df3f9d7 | ||
|
|
d1f18d36b8 | ||
|
|
5f1d46c770 | ||
|
|
9727eef476 | ||
|
|
c5be676555 | ||
|
|
f3317266dc | ||
|
|
36ff00e3f9 | ||
|
|
041063b732 | ||
|
|
2ab1dcbf1d | ||
|
|
24a0f24835 | ||
|
|
4dffab2e0a | ||
|
|
b9a5d1c573 | ||
|
|
e1818e9e31 | ||
|
|
bb2de3fdf9 | ||
|
|
82f6029043 | ||
|
|
cfaecf8b4c | ||
|
|
4595c1e32a | ||
|
|
1a648dea50 | ||
|
|
a273183745 | ||
|
|
e2243fc6d9 | ||
|
|
c1209d9f13 | ||
|
|
2b54b04cfc | ||
|
|
32fa3a0156 | ||
|
|
973e928c28 | ||
|
|
bc844246d4 | ||
|
|
41884f0a69 | ||
|
|
2a96c20739 | ||
|
|
c28eb204fb | ||
|
|
b1535aedc1 | ||
|
|
7e3feb2993 | ||
|
|
8a79a41717 | ||
|
|
530818a742 | ||
|
|
517bb3fc97 | ||
|
|
6645a1f34c | ||
|
|
2aa2963565 | ||
|
|
390220ec9c | ||
|
|
c3bd6b9384 | ||
|
|
d8e176311d | ||
|
|
1c68f0fee4 | ||
|
|
118ef01a69 | ||
|
|
148b090d8e | ||
|
|
28a96d1427 | ||
|
|
47f5e6ab89 | ||
|
|
fe09c01637 | ||
|
|
7ef3164b16 | ||
|
|
b48a32b103 | ||
|
|
2d2c408652 | ||
|
|
c381923d3e | ||
|
|
7bfddaa25a | ||
|
|
5581954fb1 | ||
|
|
c4f708b222 | ||
|
|
27056a9827 | ||
|
|
a35910429c | ||
|
|
9a7e79258c | ||
|
|
8dd3f4b119 | ||
|
|
9ecc8fc878 | ||
|
|
e078261f12 | ||
|
|
bdb3ffddd1 | ||
|
|
a72e70b026 | ||
|
|
c5eea294aa | ||
|
|
0fff404eb8 | ||
|
|
61172fa8da | ||
|
|
a6bc3f51cc | ||
|
|
1af7bf2670 | ||
|
|
d75536bf00 | ||
|
|
ce3e058f89 | ||
|
|
8d0f0b049c | ||
|
|
e619bad4a9 | ||
|
|
e6ea0647d7 | ||
|
|
d1dc271b9a | ||
|
|
f5082f3692 | ||
|
|
30c59bf387 | ||
|
|
38d0fc2c55 | ||
|
|
460e587c66 | ||
|
|
ad5a3166ac | ||
|
|
ddccd1bb61 | ||
|
|
96a690ac2f | ||
|
|
cb07189bab | ||
|
|
7e6577eb5f | ||
|
|
58ab26c4ab | ||
|
|
65d332dfd0 | ||
|
|
5eaf0b2dcd | ||
|
|
56f3735b38 | ||
|
|
23d578ac8c | ||
|
|
1bf850592c | ||
|
|
0be05795b9 | ||
|
|
08a2a91180 | ||
|
|
84cc5e57b0 | ||
|
|
5aa68e47e5 | ||
|
|
15aa4b86af | ||
|
|
114d5e1404 | ||
|
|
8ab5fe0e80 | ||
|
|
c89a6add48 | ||
|
|
888071e234 | ||
|
|
ff2e0f846a | ||
|
|
3c177d3fdc | ||
|
|
41bc490e0f | ||
|
|
f8e3742d11 | ||
|
|
a6100b39f8 |
@@ -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,vue,css,svg,sh,bash,fish}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
46
.github/CODE_OF_CONDUCT.md
vendored
46
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,46 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey@nering.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
14
.github/CONTRIBUTING.md
vendored
14
.github/CONTRIBUTING.md
vendored
@@ -1,14 +0,0 @@
|
||||
## You can find our [contribution guide on our website][contributing]
|
||||
|
||||
- Please read it carefully before opening a PR.
|
||||
- If you have any questions, you can:
|
||||
- [Open an issue][issues]
|
||||
- [Create a discussion][discussions]
|
||||
- [Chat to us on Discord][discord]
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[contributing]: https://taskfile.dev/contributing
|
||||
[issues]: https://github.com/go-task/task/issues
|
||||
[discussions]: https://github.com/go-task/task/discussions
|
||||
[discord]: https://discord.gg/6TY36E39UK
|
||||
<!-- prettier-ignore-end -->
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
github: [andreynering, pd93, vmaerten]
|
||||
open_collective: task
|
||||
custom: https://taskfile.dev/donate/
|
||||
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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 :("'
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.'
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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.
|
||||
|
||||
-->
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
|
||||
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
@@ -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"
|
||||
25
.github/renovate.json
vendored
Normal file
25
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"group:allNonMajor",
|
||||
"schedule:weekly",
|
||||
":semanticCommitTypeAll(chore)"
|
||||
],
|
||||
"mode": "full",
|
||||
"addLabels":["area: dependencies"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"addLabels": ["area: github actions"]
|
||||
},
|
||||
{
|
||||
"matchCategories": ["js", "node"],
|
||||
"addLabels": ["lang: javascript"]
|
||||
},
|
||||
{
|
||||
"matchCategories": ["golang"],
|
||||
"addLabels": ["lang: go"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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: |
|
||||
|
||||
2
.github/workflows/issue-closed.yml
vendored
2
.github/workflows/issue-closed.yml
vendored
@@ -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: |
|
||||
|
||||
28
.github/workflows/issue-experiment.yml
vendored
28
.github/workflows/issue-experiment.yml
vendored
@@ -6,10 +6,10 @@ on:
|
||||
|
||||
jobs:
|
||||
issue-experiment-proposed:
|
||||
if: github.event.label.name == format('experiment{0} proposed', ':')
|
||||
if: github.event.label.name == format('status{0} proposed', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-draft:
|
||||
if: github.event.label.name == format('experiment{0} draft', ':')
|
||||
if: github.event.label.name == format('status{0} draft', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -34,10 +34,10 @@ jobs:
|
||||
body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-candidate:
|
||||
if: github.event.label.name == format('experiment{0} candidate', ':')
|
||||
if: github.event.label.name == format('status{0} candidate', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
body: 'This experiment has been marked as a candidate! :fire: This means that the implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-stable:
|
||||
if: github.event.label.name == format('experiment{0} stable', ':')
|
||||
if: github.event.label.name == format('status{0} stable', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -62,10 +62,10 @@ jobs:
|
||||
body: 'This experiment has been marked as stable! :metal: This means that the implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-released:
|
||||
if: github.event.label.name == format('experiment{0} released', ':')
|
||||
if: github.event.label.name == format('status{0} released', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -82,10 +82,10 @@ jobs:
|
||||
state: 'closed'
|
||||
})
|
||||
issue-experiment-abandoned:
|
||||
if: github.event.label.name == format('experiment{0} abandoned', ':')
|
||||
if: github.event.label.name == format('status{0} abandoned', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -102,10 +102,10 @@ jobs:
|
||||
state: 'closed'
|
||||
})
|
||||
issue-experiment-superseded:
|
||||
if: github.event.label.name == format('experiment{0} superseded', ':')
|
||||
if: github.event.label.name == format('status{0} superseded', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-needs-triage.yml
vendored
2
.github/workflows/issue-needs-triage.yml
vendored
@@ -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: |
|
||||
|
||||
29
.github/workflows/lint.yml
vendored
29
.github/workflows/lint.yml
vendored
@@ -13,46 +13,31 @@ jobs:
|
||||
name: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v1.60.1
|
||||
version: v2.1.0
|
||||
|
||||
lint-jsonschema:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
python-version: 3.13
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: install check-jsonschema
|
||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||
|
||||
- name: check-jsonschema (metaschema)
|
||||
run: check-jsonschema --check-metaschema website/static/schema.json
|
||||
check_doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files in the docs folder
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: website/versioned_docs/**
|
||||
|
||||
- uses: actions/github-script@v7
|
||||
if: steps.changed-files-specific.outputs.any_changed == 'true'
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')
|
||||
run: check-jsonschema --check-metaschema website/src/public/schema.json
|
||||
|
||||
30
.github/workflows/release-nightly.yml
vendored
Normal file
30
.github/workflows/release-nightly.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Release nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 0 * * *
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean --nightly -f .goreleaser-nightly.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GH_PAT}}
|
||||
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
|
||||
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
|
||||
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@@ -10,17 +10,44 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.25.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
- name: npm-login
|
||||
run: |
|
||||
npm config set '//registry.npmjs.org/:_authToken'=${{ secrets.NPM_TOKEN }}
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean --draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GH_PAT}}
|
||||
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
|
||||
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
|
||||
|
||||
- name: Deploy Website
|
||||
shell: bash
|
||||
run: |
|
||||
task website:deploy:prod
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
38
.github/workflows/upload-source-documents.yml
vendored
38
.github/workflows/upload-source-documents.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Upload Source Documents
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_files_to_crowdin:
|
||||
name: Push files to Crowdin
|
||||
if: github.repository == 'go-task/task'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
website/docs
|
||||
website/blog
|
||||
website/i18n/en
|
||||
website/src/pages
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source documents
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: task crowdin:push
|
||||
env:
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
working-directory: ./website
|
||||
@@ -1,18 +1,64 @@
|
||||
# NOTE(@andreynering): The linters listed here are additions on top of
|
||||
# those enabled by default:
|
||||
#
|
||||
# https://golangci-lint.run/usage/linters/#enabled-by-default
|
||||
version: "2"
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gci
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
rewrite-rules:
|
||||
- pattern: interface{}
|
||||
replacement: any
|
||||
gofumpt:
|
||||
module-path: github.com/go-task/task/v3
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- github.com/go-task
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/go-task)
|
||||
- localmodule
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- goimports
|
||||
- gofmt
|
||||
- gofumpt
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/go-task
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
- depguard
|
||||
- mirror
|
||||
- misspell
|
||||
- noctx
|
||||
- paralleltest
|
||||
- thelper
|
||||
- tparallel
|
||||
- usetesting
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
files:
|
||||
- $all
|
||||
- '!$test'
|
||||
- '!**/errors/*.go'
|
||||
deny:
|
||||
- pkg: errors
|
||||
desc: Use github.com/go-task/task/v3/errors instead
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
15
.goreleaser-nightly.yml
Normal file
15
.goreleaser-nightly.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
pro: true
|
||||
|
||||
release:
|
||||
name_template: 'v{{.Version}}'
|
||||
|
||||
nightly:
|
||||
publish_release: true
|
||||
keep_single_release: true
|
||||
version_template: "{{incminor .Version}}-nightly"
|
||||
|
||||
includes:
|
||||
- from_file:
|
||||
path: ./.goreleaser.yml
|
||||
@@ -1,3 +1,6 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- binary: task
|
||||
main: ./cmd/task
|
||||
@@ -27,42 +30,46 @@ builds:
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w # Don't set main.version.
|
||||
- "-s -w"
|
||||
- "{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}"
|
||||
|
||||
gomod:
|
||||
proxy: true
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
|
||||
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- completion/**/*
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: [zip]
|
||||
|
||||
release:
|
||||
draft: true
|
||||
git:
|
||||
ignore_tags:
|
||||
- "{{if not .IsNightly}}nightly{{end}}"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{.Tag}}"
|
||||
version_template: '{{.Version}}'
|
||||
|
||||
checksum:
|
||||
name_template: "task_checksums.txt"
|
||||
name_template: 'task_checksums.txt'
|
||||
|
||||
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
|
||||
section: golang
|
||||
license: MIT
|
||||
conflicts:
|
||||
- taskwarrior
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
|
||||
- apk
|
||||
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
|
||||
contents:
|
||||
- src: completion/bash/task.bash
|
||||
dst: /etc/bash_completion.d/task
|
||||
@@ -80,8 +87,7 @@ brews:
|
||||
repository:
|
||||
owner: go-task
|
||||
name: homebrew-tap
|
||||
test:
|
||||
system "#{bin}/task", "--help"
|
||||
test: system "#{bin}/task", "--help"
|
||||
install: |-
|
||||
bin.install "task"
|
||||
bash_completion.install "completion/bash/task.bash" => "task"
|
||||
@@ -104,7 +110,7 @@ winget:
|
||||
commit_author:
|
||||
name: task-bot
|
||||
email: 106601941+task-bot@users.noreply.github.com
|
||||
commit_msg_template: "chore: bump {{.PackageIdentifier}} to {{.Tag}}"
|
||||
commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}'
|
||||
release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}
|
||||
tags:
|
||||
- build
|
||||
@@ -118,13 +124,49 @@ winget:
|
||||
- task-runner
|
||||
- taskfile
|
||||
- tool
|
||||
skip_upload: true
|
||||
repository:
|
||||
owner: microsoft
|
||||
owner: go-task
|
||||
name: winget-pkgs
|
||||
branch: 'chore/task-{{.Version}}'
|
||||
pull_request:
|
||||
enabled: true
|
||||
draft: false
|
||||
check_boxes: true
|
||||
base:
|
||||
owner: go-task
|
||||
owner: microsoft
|
||||
name: winget-pkgs
|
||||
branch: "bump-task-to-{{.Tag}}"
|
||||
branch: master
|
||||
|
||||
|
||||
npms:
|
||||
- name: "@go-task/cli"
|
||||
repository: "git+https://github.com/go-task/task.git"
|
||||
bugs: https://github.com/go-task/task/issues
|
||||
description: A task runner / simpler Make alternative written in Go
|
||||
homepage: https://taskfile.dev
|
||||
license: MIT
|
||||
author: "The Task authors"
|
||||
access: public
|
||||
keywords:
|
||||
- "task"
|
||||
- "taskfile"
|
||||
- "build-tool"
|
||||
- "task-runner"
|
||||
|
||||
|
||||
cloudsmiths:
|
||||
- organization: "task"
|
||||
repository: "{{if not .IsNightly}}task{{end}}"
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
- apk
|
||||
distributions:
|
||||
deb:
|
||||
- "any-distro/any-version"
|
||||
rpm:
|
||||
- "any-distro/any-version"
|
||||
alpine:
|
||||
- "alpine/any-version"
|
||||
component: main
|
||||
republish: true
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
with-expecter: true
|
||||
keeptree: true
|
||||
case: underscore
|
||||
output: ./internal/mocks
|
||||
all: False
|
||||
template: testify
|
||||
filename: '{{base (trimSuffix ".go" .InterfaceFile)}}_mock.go'
|
||||
packages:
|
||||
github.com/go-task/task/v3/internal/fingerprint:
|
||||
interfaces:
|
||||
SourcesCheckable:
|
||||
StatusCheckable:
|
||||
|
||||
4
.taskrc.yml
Normal file
4
.taskrc.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
experiments:
|
||||
GENTLE_FORCE: 0
|
||||
REMOTE_TASKFILES: 0
|
||||
ENV_PRECEDENCE: 0
|
||||
361
CHANGELOG.md
361
CHANGELOG.md
@@ -1,5 +1,346 @@
|
||||
# Changelog
|
||||
|
||||
## v3.45.2 - 2025-09-15
|
||||
|
||||
- Task now includes built-in core utilities to greatly improve compatibility on
|
||||
Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any
|
||||
other common core utility will now work by default on Windows, without extra
|
||||
setup. This is something we wanted to address for many many years, and it's
|
||||
finally being shipped!
|
||||
[Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).
|
||||
(#197, #2360 by @andreynering).
|
||||
- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)
|
||||
using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by
|
||||
@vmaerten, @andreynering, @pd93).
|
||||
- Began releasing
|
||||
[nightly builds](https://github.com/go-task/task/releases/tag/nightly). This
|
||||
will allow people to test our changes before they are fully released and
|
||||
without having to install Go to build them (#2358 by @vmaerten).
|
||||
- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or
|
||||
`$HOME/.taskrc.yml`. Check out our new
|
||||
[configuration guide](https://taskfile.dev/docs/reference/config) for more
|
||||
details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).
|
||||
- Added experiments to the taskrc schema to clarify the expected keys and values
|
||||
(#2235 by @vmaerten).
|
||||
- Added support for new properties in `.taskrc.yml`: insecure, verbose,
|
||||
concurrency, remote offline, remote timeout, and remote expiry. :warning:
|
||||
Note: setting offline via environment variable is no longer supported. (#2389
|
||||
by @vmaerten)
|
||||
- Added a `--nested` flag when outputting tasks using `--list --json`. This will
|
||||
output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).
|
||||
- Enhanced support for tasks with wildcards: they are now logged correctly, and
|
||||
wildcard parameters are fully considered during fingerprinting (#1808, #1795
|
||||
by @vmaerten).
|
||||
- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417
|
||||
by @trulede).
|
||||
|
||||
#### Package API
|
||||
|
||||
- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).
|
||||
|
||||
#### Other news
|
||||
|
||||
We recently released our
|
||||
[official GitHub Action](https://github.com/go-task/setup-task). This is based
|
||||
on the fantastic work by the Arduino team who created and maintained the
|
||||
community version. Now that this is officially adopted, fixes/updates should be
|
||||
more timely. We have already merged a couple of longstanding PRs in our
|
||||
[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by
|
||||
@pd93, @shrink, @trim21 and all the previous contributors to
|
||||
[arduino/setup-task](https://github.com/arduino/setup-task/)).
|
||||
|
||||
## v3.45.0-v3.45.1 - 2025-09-15
|
||||
|
||||
Failed due to an issue with our release process.
|
||||
|
||||
## v3.44.1 - 2025-07-23
|
||||
|
||||
- Internal tasks will no longer be shown as suggestions since they cannot be
|
||||
called (#2309, #2323 by @maxmzkrcensys)
|
||||
- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).
|
||||
- Fixed a regression where fingerprinting was not working correctly if the path
|
||||
to you Taskfile contained a space (#2321, #2322 by @pd93).
|
||||
- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).
|
||||
- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests
|
||||
(#2265 by @pd93).
|
||||
|
||||
## v3.44.0 - 2025-06-08
|
||||
|
||||
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
|
||||
@pd93).
|
||||
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
|
||||
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
|
||||
string). (#2138, #2139, #2140 by @pd93).
|
||||
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
|
||||
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
|
||||
- Added the ability to
|
||||
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
|
||||
by specifying a checksum. This works with both local and remote Taskfiles
|
||||
(#2222, #2223 by @pd93).
|
||||
- When using the
|
||||
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
|
||||
any credentials used in the URL will now be redacted in Task's output (#2100,
|
||||
#2220 by @pd93).
|
||||
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
|
||||
by @vmaerten).
|
||||
- Fixed a bug where taskfiles in directories containing spaces created
|
||||
directories in the wrong location (#2208, #2216 by @pd93).
|
||||
- Added support for dual JSON schema files, allowing changes without affecting
|
||||
the current schema. The current schemas will only be updated during releases.
|
||||
(#2211 by @vmaerten).
|
||||
- Improved fingerprint documentation by specifying that the method can be set at
|
||||
the root level to apply to all tasks (#2233 by @vmaerten).
|
||||
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
|
||||
@wazazaby, #2271 by @andreynering).
|
||||
|
||||
## v3.43.3 - 2025-04-27
|
||||
|
||||
Reverted the changes made in #2113 and #2186 that affected the
|
||||
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
|
||||
#2208.
|
||||
|
||||
## v3.43.2 - 2025-04-21
|
||||
|
||||
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by
|
||||
@vmaerten).
|
||||
|
||||
## v3.43.1 - 2025-04-21
|
||||
|
||||
- Significant improvements were made to the watcher. We migrated from
|
||||
[watcher](https://github.com/radovskyb/watcher) to
|
||||
[fsnotify](https://github.com/fsnotify/fsnotify). The former library used
|
||||
polling, which means Task had a high CPU usage when watching too many files.
|
||||
`fsnotify` uses proper the APIs from each operating system to watch files,
|
||||
which means a much better performance. The default interval changed from 5
|
||||
seconds to 100 milliseconds, because now it configures the wait time for
|
||||
duplicated events, instead of the polling time (#2048 by @andreynering, #1508,
|
||||
#985, #1179).
|
||||
- The [Map Variables experiment](https://github.com/go-task/task/issues/1585)
|
||||
was made generally available so you can now
|
||||
[define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables)
|
||||
(#1585, #1547, #2081 by @pd93).
|
||||
- Wildcards can now
|
||||
[match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072,
|
||||
#2121 by @pd93).
|
||||
- Added the ability to
|
||||
[loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files).
|
||||
This works the same way as looping over sources (#2151 by @sedyh).
|
||||
- Added the ability to resolve variables when defining an include variable
|
||||
(#2108, #2113 by @pd93).
|
||||
- A few changes have been made to the
|
||||
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317)
|
||||
(#1402, #2176 by @pd93):
|
||||
- Cached files are now prioritized over remote ones.
|
||||
- Added an `--expiry` flag which sets the TTL for a remote file cache. By
|
||||
default the value will be 0 (caching disabled). If Task is running in
|
||||
offline mode or fails to make a connection, it will fallback on the cache.
|
||||
- `.taskrc` files can now be used from subdirectories and will be searched for
|
||||
recursively up the file tree in the same way that Taskfiles are (#2159, #2166
|
||||
by @pd93).
|
||||
- The default taskfile (output when using the `--init` flag) is now an embedded
|
||||
file in the binary instead of being stored in the code (#2112 by @pd93).
|
||||
- Improved the way we report the Task version when using the `--version` flag or
|
||||
`{{.TASK_VERSION}}` variable. This should now be more consistent and easier
|
||||
for package maintainers to use (#2131 by @pd93).
|
||||
- Fixed a bug where globstar (`**`) matching in `sources` only resolved the
|
||||
first result (#2073, #2075 by @pd93).
|
||||
- Fixed a bug where sorting tasks by "none" would use the default sorting
|
||||
instead of leaving tasks in the order they were defined (#2124, #2125 by
|
||||
@trulede).
|
||||
- Fixed Fish completion on newer Fish versions (#2130 by @atusy).
|
||||
- Fixed a bug where undefined/null variables resolved to an empty string instead
|
||||
of `nil` (#1911, #2144 by @pd93).
|
||||
- The `USER_WORKING_DIR` special now will now properly account for the `--dir`
|
||||
(`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering).
|
||||
- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).
|
||||
- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).
|
||||
|
||||
#### Package API
|
||||
|
||||
- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now
|
||||
uses the functional options pattern (#2085, #2147, #2148 by @pd93).
|
||||
- The functional options for the
|
||||
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
|
||||
and
|
||||
[`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
|
||||
types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by
|
||||
@pd93).
|
||||
- [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
|
||||
no longer accepts a
|
||||
[`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node).
|
||||
Instead nodes are passed directly into the
|
||||
[`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
|
||||
method (#2169 by @pd93).
|
||||
- [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
|
||||
also now accepts a [`context.Context`](https://pkg.go.dev/context#Context)
|
||||
(#2176 by @pd93).
|
||||
|
||||
## v3.42.1 - 2025-03-10
|
||||
|
||||
- Fixed a bug where some special variables caused a type error when used global
|
||||
variables (#2106, #2107 by @pd93).
|
||||
|
||||
## v3.42.0 - 2025-03-08
|
||||
|
||||
- Made `--init` less verbose by default and respect `--silent` and `--verbose`
|
||||
flags (#2009, #2011 by @HeCorr).
|
||||
- `--init` now accepts a file name or directory as an argument (#2008, #2018 by
|
||||
@HeCorr).
|
||||
- Fix a bug where an HTTP node's location was being mutated incorrectly (#2007
|
||||
by @jeongukjae).
|
||||
- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by
|
||||
@vmaerten).
|
||||
- Use only the relevant checker (timestamp or checksum) to improve performance
|
||||
(#2029, #2031 by @vmaerten).
|
||||
- Print warnings when attempting to enable an inactive experiment or an active
|
||||
experiment with an invalid value (#1979, #2049 by @pd93).
|
||||
- Refactored the experiments package and added tests (#2049 by @pd93).
|
||||
- Show allowed values when a variable with an enum is missing (#2027, #2052 by
|
||||
@vmaerten).
|
||||
- Refactored how snippets in error work and added tests (#2068 by @pd93).
|
||||
- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by
|
||||
@pd93).
|
||||
- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand
|
||||
`cmds` syntax were not considered valid (#2068 by @pd93).
|
||||
- Refactored how task sorting functions work (#1798 by @pd93).
|
||||
- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable
|
||||
experiments (similar to `.env`) (#1982 by @vmaerten).
|
||||
- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086
|
||||
by @pd93).
|
||||
- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).
|
||||
- Fixed a bug where, when a dynamic variable is provided, even if it is not
|
||||
used, all other variables become unavailable in the templating system within
|
||||
the include (#2092 by @vmaerten).
|
||||
|
||||
#### Package API
|
||||
|
||||
Unlike our CLI tool,
|
||||
[Task's package API is not currently stable](https://taskfile.dev/reference/package).
|
||||
In an effort to ease the pain of breaking changes for our users, we will be
|
||||
providing changelogs for our package API going forwards. The hope is that these
|
||||
changes will provide a better long-term experience for our users and allow to
|
||||
stabilize the API in the future. #121 now tracks this piece of work.
|
||||
|
||||
- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).
|
||||
- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)
|
||||
(#2011, ff8c913 by @HeCorr and @pd93)
|
||||
- No longer accepts an `io.Writer` (output is now the caller's
|
||||
responsibility).
|
||||
- The path argument can now be a filename OR a directory.
|
||||
- The function now returns the full path of the generated file.
|
||||
- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)
|
||||
now accepts a string instead of the arguments required to generate a snippet
|
||||
(#2068 by @pd93).
|
||||
- The caller is now expected to create the snippet themselves (see below).
|
||||
- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
|
||||
and related code moved from the `errors` package to the `taskfile` package
|
||||
(#2068 by @pd93).
|
||||
- Renamed `TaskMissingRequiredVars` to
|
||||
[`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)
|
||||
(#2052 by @vmaerten).
|
||||
- Renamed `TaskNotAllowedVars` to
|
||||
[`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)
|
||||
(#2052 by @vmaerten).
|
||||
- The
|
||||
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
|
||||
is now constructed using the functional options pattern (#2082 by @pd93).
|
||||
- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082
|
||||
by @pd93).
|
||||
- Users are now expected to pass a custom debug/prompt functions into
|
||||
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
|
||||
if they want this functionality by using the new
|
||||
[`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)
|
||||
and
|
||||
[`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)
|
||||
functional options.
|
||||
- Remove `Range` functions in the `taskfile/ast` package in favour of new
|
||||
iterator functions (#1798 by @pd93).
|
||||
- `ast.Call` was moved from the `taskfile/ast` package to the main `task`
|
||||
package (#2084 by @pd93).
|
||||
- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the
|
||||
`task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).
|
||||
- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were
|
||||
moved from the `internal/compiler` package to the main `task` package (#2084
|
||||
by @pd93).
|
||||
|
||||
## v3.41.0 - 2025-01-18
|
||||
|
||||
- Fixed an issue where dynamic variables were not properly logged in verbose
|
||||
mode (#1920, #1921 by @mgbowman).
|
||||
- Support `silent` for defer statements (#1877, #1879 by @danilobuerger).
|
||||
- Added an option to exclude some tasks from being included (#1859 by
|
||||
@vmaerten).
|
||||
- Fixed an issue where a required variable was incorrectly handled in a template
|
||||
function (#1950, #1962 by @vmaerten).
|
||||
- Expose a new `TASK_DIR` special variable, which will contain the absolute path
|
||||
of task directory. (#1959, #1961 by @vmaerten).
|
||||
- Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by
|
||||
@pd93, @GrahamDennis and @trim21).
|
||||
- Refactored internal ordered map implementation to use
|
||||
[github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap)
|
||||
(#1797 by @pd93).
|
||||
- Fixed a bug where variables defined at the task level were being ignored in
|
||||
the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko)
|
||||
- The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds`
|
||||
(#1872 by @niklasr22).
|
||||
- Updated [installation docs](https://taskfile.dev/installation) and added pip
|
||||
installation method (#935, #1989 by @pd93).
|
||||
- Fixed a bug where dynamic variables could not access environment variables
|
||||
(#630, #1869 by @rohm1 and @pd93).
|
||||
- Disable version check for use as an external library (#1938 by @leaanthony).
|
||||
|
||||
## v3.40.1 - 2024-12-06
|
||||
|
||||
- Fixed a security issue in `git-urls` by switching to the maintained fork
|
||||
`chainguard-dev/git-urls` (#1917 by @AlekSi).
|
||||
- Added missing `platforms` property to `cmds` that use `for` (#1915 by
|
||||
@dkarter).
|
||||
- Added misspell linter to check for misspelled English words (#1883 by
|
||||
@christiandins).
|
||||
|
||||
## 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
|
||||
up to date (#1783 by @vmaerten).
|
||||
- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).
|
||||
- Fixed a bug where ZSH and PowerShell completions did not work when using the
|
||||
recommended method. (#1813, #1809 by @vmaerten and @shirayu)
|
||||
- Fix variables not working properly for a `defer:` statement (#1803, #1814 by
|
||||
@vmaerten and @andreynering).
|
||||
|
||||
## v3.39.0 - 2024-09-07
|
||||
|
||||
- Added
|
||||
@@ -249,8 +590,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).
|
||||
|
||||
@@ -309,7 +650,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
|
||||
@@ -324,7 +665,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
|
||||
@@ -397,8 +738,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
|
||||
|
||||
@@ -753,7 +1094,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
|
||||
@@ -762,7 +1103,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
|
||||
@@ -904,7 +1245,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
|
||||
@@ -964,7 +1305,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)
|
||||
|
||||
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="https://taskfile.dev">
|
||||
<img src="website/static/img/logo.svg" width="200px" height="200px" />
|
||||
<img src="website/src/public/img/logo.svg" width="200px" height="200px" />
|
||||
</a>
|
||||
|
||||
<h1>Task</h1>
|
||||
@@ -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="https://devowl.io/wp-content/uploads/meta/favicon.webp" height="100px" title="devowl.io" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
109
Taskfile.yml
109
Taskfile.yml
@@ -8,6 +8,7 @@ includes:
|
||||
|
||||
vars:
|
||||
BIN: "{{.ROOT_DIR}}/bin"
|
||||
GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}'
|
||||
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
@@ -18,6 +19,11 @@ tasks:
|
||||
- task: lint
|
||||
- task: test
|
||||
|
||||
run:
|
||||
desc: Runs Task
|
||||
cmds:
|
||||
- go run ./cmd/task {{.CLI_ARGS}}
|
||||
|
||||
install:
|
||||
desc: Installs Task
|
||||
aliases: [i]
|
||||
@@ -27,27 +33,44 @@ tasks:
|
||||
- go install -v ./cmd/task
|
||||
|
||||
generate:
|
||||
desc: Runs Go generate to create mocks
|
||||
aliases: [gen, g]
|
||||
desc: Runs all generate tasks
|
||||
cmds:
|
||||
- task: generate:mocks
|
||||
- task: generate:fixtures
|
||||
|
||||
generate:mocks:
|
||||
desc: Runs Mockery to create mocks
|
||||
aliases: [gen:mocks, g:mocks]
|
||||
deps: [install:mockery]
|
||||
sources:
|
||||
- "internal/fingerprint/checker.go"
|
||||
generates:
|
||||
- "internal/mocks/*.go"
|
||||
cmds:
|
||||
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name SourcesCheckable"
|
||||
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name StatusCheckable"
|
||||
- find . -type f -name *_mock.go -delete
|
||||
- "{{.BIN}}/mockery"
|
||||
|
||||
generate:fixtures:
|
||||
desc: Runs tests and generates golden fixture files
|
||||
aliases: [gen:fixtures, g:fixtures]
|
||||
env:
|
||||
GOLDIE_UPDATE: 'true'
|
||||
GOLDIE_TEMPLATE: 'true'
|
||||
cmds:
|
||||
- find ./testdata -name '*.golden' -delete
|
||||
- go test ./...
|
||||
|
||||
install:mockery:
|
||||
desc: Installs mockgen; a tool to generate mock files
|
||||
vars:
|
||||
MOCKERY_VERSION: v2.24.0
|
||||
MOCKERY_VERSION: v3.2.2
|
||||
env:
|
||||
GOBIN: "{{.BIN}}"
|
||||
status:
|
||||
- go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}}
|
||||
cmds:
|
||||
- go install github.com/vektra/mockery/v2@{{.MOCKERY_VERSION}}
|
||||
- GOBIN="{{.BIN}}" go install github.com/vektra/mockery/v3@{{.MOCKERY_VERSION}}
|
||||
|
||||
mod:
|
||||
desc: Downloads and tidy Go modules
|
||||
@@ -57,6 +80,7 @@ tasks:
|
||||
|
||||
clean:
|
||||
desc: Cleans temp files and folders
|
||||
aliases: [clear]
|
||||
cmds:
|
||||
- rm -rf dist/
|
||||
- rm -rf tmp/
|
||||
@@ -67,6 +91,7 @@ tasks:
|
||||
sources:
|
||||
- './**/*.go'
|
||||
- .golangci.yml
|
||||
- go.mod
|
||||
cmds:
|
||||
- golangci-lint run
|
||||
|
||||
@@ -75,9 +100,19 @@ tasks:
|
||||
sources:
|
||||
- './**/*.go'
|
||||
- .golangci.yml
|
||||
- go.mod
|
||||
cmds:
|
||||
- golangci-lint run --fix
|
||||
|
||||
format:
|
||||
desc: Runs golangci-lint and formats any Go files
|
||||
aliases: [fmt, f]
|
||||
sources:
|
||||
- './**/*.go'
|
||||
- .golangci.yml
|
||||
cmds:
|
||||
- golangci-lint fmt
|
||||
|
||||
sleepit:build:
|
||||
desc: Builds the sleepit test helper
|
||||
sources:
|
||||
@@ -97,31 +132,57 @@ tasks:
|
||||
test:
|
||||
desc: Runs test suite
|
||||
aliases: [t]
|
||||
deps: [install]
|
||||
deps: [gotestsum:install]
|
||||
sources:
|
||||
- "**/*.go"
|
||||
- "testdata/**/*"
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}}
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./...
|
||||
|
||||
test:watch:
|
||||
desc: Runs test suite with watch tests included
|
||||
deps: [sleepit:build, gotestsum:install]
|
||||
cmds:
|
||||
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch'
|
||||
|
||||
test:all:
|
||||
desc: Runs test suite with signals and watch tests included
|
||||
deps: [install, sleepit:build]
|
||||
deps: [sleepit:build, gotestsum:install]
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
|
||||
|
||||
goreleaser:test:
|
||||
desc: Tests release process without publishing
|
||||
cmds:
|
||||
- goreleaser --snapshot --clean
|
||||
|
||||
gotestsum:install:
|
||||
desc: Installs gotestsum
|
||||
status:
|
||||
- command -v gotestsum
|
||||
cmds:
|
||||
- go install gotest.tools/gotestsum@latest
|
||||
|
||||
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
|
||||
@@ -136,7 +197,7 @@ tasks:
|
||||
- Push the commit/tag to the repository
|
||||
- Create a GitHub release
|
||||
|
||||
To use the task, simply run "task release:<version>" where "<version>" is is one of:
|
||||
To use the task, run "task release:<version>" where "<version>" is is one of:
|
||||
|
||||
- "major" - Bumps the major number
|
||||
- "minor" - Bumps the minor number
|
||||
@@ -151,7 +212,6 @@ tasks:
|
||||
Please wait for the CI to finish and then do the following:
|
||||
|
||||
- Copy the changelog for v{{.VERSION}} to the GitHub release
|
||||
- Publish the package to NPM with `task npm:publish`
|
||||
- Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
|
||||
preconditions:
|
||||
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main"
|
||||
@@ -170,16 +230,3 @@ tasks:
|
||||
- "git push origin tag v{{.VERSION}}"
|
||||
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
|
||||
silent: true
|
||||
|
||||
npm:publish:
|
||||
desc: Publish release to npm
|
||||
cmds:
|
||||
- npm publish --access=public
|
||||
|
||||
packages:
|
||||
cmds:
|
||||
- echo '{{.GO_PACKAGES}}'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
silent: true
|
||||
|
||||
37
args/args.go
37
args/args.go
@@ -3,17 +3,34 @@ package args
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
// Get fetches the remaining arguments after CLI parsing and splits them into
|
||||
// two groups: the arguments before the double dash (--) and the arguments after
|
||||
// the double dash.
|
||||
func Get() ([]string, []string, error) {
|
||||
args := pflag.Args()
|
||||
doubleDashPos := pflag.CommandLine.ArgsLenAtDash()
|
||||
|
||||
if doubleDashPos == -1 {
|
||||
return args, nil, nil
|
||||
}
|
||||
return args[:doubleDashPos], args[doubleDashPos:], nil
|
||||
}
|
||||
|
||||
// Parse parses command line argument: tasks and global variables
|
||||
func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
|
||||
calls := []*ast.Call{}
|
||||
globals := &ast.Vars{}
|
||||
func Parse(args ...string) ([]*task.Call, *ast.Vars) {
|
||||
calls := []*task.Call{}
|
||||
globals := ast.NewVars()
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "=") {
|
||||
calls = append(calls, &ast.Call{Task: arg})
|
||||
calls = append(calls, &task.Call{Task: arg})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -24,6 +41,18 @@ func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
|
||||
return calls, globals
|
||||
}
|
||||
|
||||
func ToQuotedString(args []string) (string, error) {
|
||||
var quotedCliArgs []string
|
||||
for _, arg := range args {
|
||||
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
|
||||
}
|
||||
return strings.Join(quotedCliArgs, " "), nil
|
||||
}
|
||||
|
||||
func splitVar(s string) (string, string) {
|
||||
pair := strings.SplitN(s, "=", 2)
|
||||
return pair[0], pair[1]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,30 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/otiai10/copy"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
changelogSource = "CHANGELOG.md"
|
||||
changelogTarget = "website/docs/changelog.mdx"
|
||||
docsSource = "website/docs"
|
||||
docsTarget = "website/versioned_docs/version-latest"
|
||||
changelogTarget = "website/src/docs/changelog.md"
|
||||
versionFile = "internal/version/version.txt"
|
||||
)
|
||||
|
||||
var (
|
||||
changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
|
||||
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
|
||||
)
|
||||
var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
|
||||
|
||||
// Flags
|
||||
var (
|
||||
@@ -48,7 +43,7 @@ func release() error {
|
||||
return errors.New("error: expected version number")
|
||||
}
|
||||
|
||||
version, err := getVersion()
|
||||
version, err := getVersion(versionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -66,28 +61,18 @@ func release() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setJSONVersion("package.json", version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setJSONVersion("package-lock.json", version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := docs(); err != nil {
|
||||
if err := setVersionFile(versionFile, version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVersion() (*semver.Version, error) {
|
||||
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
||||
b, err := cmd.Output()
|
||||
func getVersion(filename string) (*semver.Version, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return semver.NewVersion(strings.TrimSpace(string(b)))
|
||||
}
|
||||
|
||||
@@ -143,26 +128,6 @@ func changelog(version *semver.Version) error {
|
||||
return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
|
||||
}
|
||||
|
||||
func setJSONVersion(fileName string, version *semver.Version) error {
|
||||
// Read the JSON file
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace the version
|
||||
new := versionRegex.ReplaceAllString(string(b), fmt.Sprintf(` "version": "%s",`, version.String()))
|
||||
|
||||
// Write the JSON file
|
||||
return os.WriteFile(fileName, []byte(new), 0o644)
|
||||
}
|
||||
|
||||
func docs() error {
|
||||
if err := os.RemoveAll(docsTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copy.Copy(docsSource, docsTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
func setVersionFile(fileName string, version *semver.Version) error {
|
||||
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
169
cmd/task/task.go
169
cmd/task/task.go
@@ -4,20 +4,18 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/args"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/flags"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
ver "github.com/go-task/task/v3/internal/version"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/go-task/task/v3/internal/version"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -44,7 +42,7 @@ func main() {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
logger := &logger.Logger{
|
||||
log := &logger.Logger{
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Verbose: flags.Verbose,
|
||||
@@ -55,11 +53,12 @@ func run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := flags.Dir
|
||||
entrypoint := flags.Entrypoint
|
||||
if err := experiments.Validate(); err != nil {
|
||||
log.Warnf("%s\n", err.Error())
|
||||
}
|
||||
|
||||
if flags.Version {
|
||||
fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
|
||||
fmt.Println(version.GetVersionWithBuildInfo())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,7 +68,7 @@ func run() error {
|
||||
}
|
||||
|
||||
if flags.Experiments {
|
||||
return experiments.List(logger)
|
||||
return log.PrintExperiments()
|
||||
}
|
||||
|
||||
if flags.Init {
|
||||
@@ -77,9 +76,28 @@ func run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
|
||||
args, _, err := args.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := wd
|
||||
if len(args) > 0 {
|
||||
name := args[0]
|
||||
if filepathext.IsExtOnly(name) {
|
||||
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
|
||||
}
|
||||
path = filepathext.SmartJoin(wd, name)
|
||||
}
|
||||
finalPath, err := task.InitTaskfile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !flags.Silent {
|
||||
if flags.Verbose {
|
||||
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
|
||||
}
|
||||
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,81 +110,30 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.Global {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("task: Failed to get user home directory: %w", err)
|
||||
}
|
||||
dir = home
|
||||
}
|
||||
|
||||
var taskSorter sort.TaskSorter
|
||||
switch flags.TaskSort {
|
||||
case "none":
|
||||
taskSorter = &sort.Noop{}
|
||||
case "alphanumeric":
|
||||
taskSorter = &sort.AlphaNumeric{}
|
||||
}
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Entrypoint: entrypoint,
|
||||
Force: flags.Force,
|
||||
ForceAll: flags.ForceAll,
|
||||
Insecure: flags.Insecure,
|
||||
Download: flags.Download,
|
||||
Offline: flags.Offline,
|
||||
Timeout: flags.Timeout,
|
||||
Watch: flags.Watch,
|
||||
Verbose: flags.Verbose,
|
||||
Silent: flags.Silent,
|
||||
AssumeYes: flags.AssumeYes,
|
||||
Dry: flags.Dry || flags.Status,
|
||||
Summary: flags.Summary,
|
||||
Parallel: flags.Parallel,
|
||||
Color: flags.Color,
|
||||
Concurrency: flags.Concurrency,
|
||||
Interval: flags.Interval,
|
||||
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
|
||||
OutputStyle: flags.Output,
|
||||
TaskSorter: taskSorter,
|
||||
}
|
||||
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
|
||||
if err := listOptions.Validate(); err != nil {
|
||||
e := task.NewExecutor(
|
||||
flags.WithFlags(),
|
||||
task.WithVersionCheck(true),
|
||||
)
|
||||
if err := e.Setup(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := e.Setup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if experiments.AnyVariables.Enabled {
|
||||
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
|
||||
}
|
||||
|
||||
// If the download flag is specified, we should stop execution as soon as
|
||||
// taskfile is downloaded
|
||||
if flags.Download {
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.ClearCache {
|
||||
cache, err := taskfile.NewCache(e.TempDir.Remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cache.Clear()
|
||||
}
|
||||
|
||||
if (listOptions.ShouldListTasks()) && flags.Silent {
|
||||
return e.ListTaskNames(flags.ListAll)
|
||||
cachePath := filepath.Join(e.TempDir.Remote, "remote")
|
||||
return os.RemoveAll(cachePath)
|
||||
}
|
||||
|
||||
listOptions := task.NewListOptions(
|
||||
flags.List,
|
||||
flags.ListAll,
|
||||
flags.ListJson,
|
||||
flags.NoStatus,
|
||||
flags.Nested,
|
||||
)
|
||||
if listOptions.ShouldListTasks() {
|
||||
if flags.Silent {
|
||||
return e.ListTaskNames(flags.ListAll)
|
||||
}
|
||||
foundTasks, err := e.ListTasks(listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -177,27 +144,28 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
calls []*ast.Call
|
||||
globals *ast.Vars
|
||||
)
|
||||
|
||||
tasksAndVars, cliArgs, err := getArgs()
|
||||
// Parse the remaining arguments
|
||||
cliArgsPreDash, cliArgsPostDash, err := args.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calls, globals = args.Parse(tasksAndVars...)
|
||||
calls, globals := args.Parse(cliArgsPreDash...)
|
||||
|
||||
// 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})
|
||||
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
|
||||
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
|
||||
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 {
|
||||
@@ -212,24 +180,3 @@ func run() error {
|
||||
|
||||
return e.Run(ctx, calls...)
|
||||
}
|
||||
|
||||
func getArgs() ([]string, string, error) {
|
||||
var (
|
||||
args = pflag.Args()
|
||||
doubleDashPos = pflag.CommandLine.ArgsLenAtDash()
|
||||
)
|
||||
|
||||
if doubleDashPos == -1 {
|
||||
return args, "", nil
|
||||
}
|
||||
|
||||
var quotedCliArgs []string
|
||||
for _, arg := range args[doubleDashPos:] {
|
||||
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
|
||||
}
|
||||
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
|
||||
}
|
||||
|
||||
38
cmd/tmp/main.go
Normal file
38
cmd/tmp/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
defer cancel()
|
||||
if err := run(ctx); err != nil {
|
||||
fmt.Println(ctx.Err())
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
req, err := http.NewRequest("GET", "https://taskfile.dev/schema.json", nil)
|
||||
if err != nil {
|
||||
fmt.Println(1)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
fmt.Println(2)
|
||||
return err
|
||||
}
|
||||
fmt.Println(3)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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 || newVar.Sh == nil {
|
||||
result.Set(k, ast.Var{Value: newVar.Value})
|
||||
return nil
|
||||
}
|
||||
// If the variable is dynamic, we need to resolve it first
|
||||
static, err := c.HandleDynamicVar(newVar, dir)
|
||||
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -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,30 @@ 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)
|
||||
} else {
|
||||
allVars["TASK"] = ""
|
||||
allVars["TASK_DIR"] = ""
|
||||
allVars["TASKFILE"] = ""
|
||||
allVars["TASKFILE_DIR"] = ""
|
||||
}
|
||||
if call != nil {
|
||||
allVars["ALIAS"] = call.Task
|
||||
} else {
|
||||
allVars["ALIAS"] = ""
|
||||
}
|
||||
|
||||
return allVars, nil
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
set GO_TASK_PROGNAME task
|
||||
set -l GO_TASK_PROGNAME task
|
||||
|
||||
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
|
||||
# Check if the global task is requested
|
||||
set -l global_task false
|
||||
commandline --current-process | read --tokenize --list --local cmd_args
|
||||
for arg in $cmd_args
|
||||
if test "_$arg" = "_--"
|
||||
break # ignore arguments to be passed to the task
|
||||
end
|
||||
if test "_$arg" = "_--global" -o "_$arg" = "_-g"
|
||||
set global_task true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
function __task_get_tasks --description "Prints all available tasks with their description"
|
||||
# Read the list of tasks (and potential errors)
|
||||
$GO_TASK_PROGNAME --list-all 2>&1 | read -lz rawOutput
|
||||
if $global_task
|
||||
$GO_TASK_PROGNAME --global --list-all
|
||||
else
|
||||
$GO_TASK_PROGNAME --list-all
|
||||
end 2>&1 | read -lz rawOutput
|
||||
|
||||
# Return on non-zero exit code (for cases when there is no Taskfile found or etc.)
|
||||
if test $status -ne 0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#compdef task
|
||||
|
||||
local context state state_descr line
|
||||
compdef _task task
|
||||
typeset -A opt_args
|
||||
|
||||
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
|
||||
@@ -39,26 +38,33 @@ function __task_list() {
|
||||
_describe 'Task to run' scripts
|
||||
}
|
||||
|
||||
_arguments \
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
|
||||
'(-c --color)'{-c,--color}'[colored output]' \
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]' \
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
|
||||
+ '(operation)' \
|
||||
{-l,--list}'[list describable tasks]' \
|
||||
{-a,--list-all}'[list all tasks]' \
|
||||
{-i,--init}'[create new Taskfile.yml]' \
|
||||
'(-*)'{-h,--help}'[show help]' \
|
||||
'(-*)--version[show version and exit]' \
|
||||
'*: :__task_list'
|
||||
_task() {
|
||||
_arguments \
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
|
||||
'(-c --color)'{-c,--color}'[colored output]' \
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]' \
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
|
||||
+ '(operation)' \
|
||||
{-l,--list}'[list describable tasks]' \
|
||||
{-a,--list-all}'[list all tasks]' \
|
||||
{-i,--init}'[create new Taskfile.yml]' \
|
||||
'(-*)'{-h,--help}'[show help]' \
|
||||
'(-*)--version[show version and exit]' \
|
||||
'*: :__task_list'
|
||||
}
|
||||
|
||||
# don't run the completion function when being source-ed or eval-ed
|
||||
if [ "$funcstack[1]" = "_task" ]; then
|
||||
_task "$@"
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ const (
|
||||
CodeUnknown // Used when no other exit code is appropriate
|
||||
)
|
||||
|
||||
// TaskRC related exit codes
|
||||
const (
|
||||
CodeTaskRCNotFoundError int = iota + 50
|
||||
)
|
||||
|
||||
// Taskfile related exit codes
|
||||
const (
|
||||
CodeTaskfileNotFound int = iota + 100
|
||||
@@ -21,6 +26,7 @@ const (
|
||||
CodeTaskfileNetworkTimeout
|
||||
CodeTaskfileInvalid
|
||||
CodeTaskfileCycle
|
||||
CodeTaskfileDoesNotMatchChecksum
|
||||
)
|
||||
|
||||
// Task related exit codes
|
||||
@@ -32,6 +38,7 @@ const (
|
||||
CodeTaskCalledTooManyTimes
|
||||
CodeTaskCancelled
|
||||
CodeTaskMissingRequiredVars
|
||||
CodeTaskNotAllowedVars
|
||||
)
|
||||
|
||||
// TaskError extends the standard error interface with a Code method. This code will
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -46,8 +47,9 @@ func (err *TaskRunError) Code() int {
|
||||
}
|
||||
|
||||
func (err *TaskRunError) TaskExitCode() int {
|
||||
if c, ok := interp.IsExitStatus(err.Err); ok {
|
||||
return int(c)
|
||||
var exit interp.ExitStatus
|
||||
if errors.As(err.Err, &exit) {
|
||||
return int(exit)
|
||||
}
|
||||
return err.Code()
|
||||
}
|
||||
@@ -141,20 +143,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 {
|
||||
vars := make([]string, 0, len(err.MissingVars))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -155,19 +155,14 @@ func (err *TaskfileVersionCheckError) Code() int {
|
||||
// TaskfileNetworkTimeoutError is returned when the user attempts to use a remote
|
||||
// Taskfile but a network connection could not be established within the timeout.
|
||||
type TaskfileNetworkTimeoutError struct {
|
||||
URI string
|
||||
Timeout time.Duration
|
||||
CheckedCache bool
|
||||
URI string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (err *TaskfileNetworkTimeoutError) Error() string {
|
||||
var cacheText string
|
||||
if err.CheckedCache {
|
||||
cacheText = " and no offline copy was found in the cache"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
`task: Network connection timed out after %s while attempting to download Taskfile %q%s`,
|
||||
err.Timeout, err.URI, cacheText,
|
||||
`task: Network connection timed out after %s while attempting to download Taskfile %q`,
|
||||
err.Timeout, err.URI,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
|
||||
func (err TaskfileCycleError) Code() int {
|
||||
return CodeTaskfileCycle
|
||||
}
|
||||
|
||||
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
|
||||
// match the one pinned in the parent Taskfile.
|
||||
type TaskfileDoesNotMatchChecksum struct {
|
||||
URI string
|
||||
ExpectedChecksum string
|
||||
ActualChecksum string
|
||||
}
|
||||
|
||||
func (err *TaskfileDoesNotMatchChecksum) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
|
||||
err.URI,
|
||||
err.ActualChecksum,
|
||||
err.ExpectedChecksum,
|
||||
)
|
||||
}
|
||||
|
||||
func (err *TaskfileDoesNotMatchChecksum) Code() int {
|
||||
return CodeTaskfileDoesNotMatchChecksum
|
||||
}
|
||||
|
||||
20
errors/errors_taskrc.go
Normal file
20
errors/errors_taskrc.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type TaskRCNotFoundError struct {
|
||||
URI string
|
||||
Walk bool
|
||||
}
|
||||
|
||||
func (err TaskRCNotFoundError) Error() string {
|
||||
var walkText string
|
||||
if err.Walk {
|
||||
walkText = " (or any of the parent directories)"
|
||||
}
|
||||
return fmt.Sprintf(`task: No Task config file found at %q%s`, err.URI, walkText)
|
||||
}
|
||||
|
||||
func (err TaskRCNotFoundError) Code() int {
|
||||
return CodeTaskRCNotFoundError
|
||||
}
|
||||
504
executor.go
Normal file
504
executor.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/sajari/fuzzy"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type (
|
||||
// An ExecutorOption is any type that can apply a configuration to an
|
||||
// [Executor].
|
||||
ExecutorOption interface {
|
||||
ApplyToExecutor(*Executor)
|
||||
}
|
||||
// An Executor is used for processing Taskfile(s) and executing the task(s)
|
||||
// within them.
|
||||
Executor struct {
|
||||
// Flags
|
||||
Dir string
|
||||
Entrypoint string
|
||||
TempDir TempDir
|
||||
Force bool
|
||||
ForceAll bool
|
||||
Insecure bool
|
||||
Download bool
|
||||
Offline bool
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
AssumeYes bool
|
||||
AssumeTerm bool // Used for testing
|
||||
Dry bool
|
||||
Summary bool
|
||||
Parallel bool
|
||||
Color bool
|
||||
Concurrency int
|
||||
Interval time.Duration
|
||||
|
||||
// I/O
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
// Internal
|
||||
Taskfile *ast.Taskfile
|
||||
Logger *logger.Logger
|
||||
Compiler *Compiler
|
||||
Output output.Output
|
||||
OutputStyle ast.Output
|
||||
TaskSorter sort.Sorter
|
||||
UserWorkingDir string
|
||||
EnableVersionCheck bool
|
||||
|
||||
fuzzyModel *fuzzy.Model
|
||||
|
||||
concurrencySemaphore chan struct{}
|
||||
taskCallCount map[string]*int32
|
||||
mkdirMutexMap map[string]*sync.Mutex
|
||||
executionHashes map[string]context.Context
|
||||
executionHashesMutex sync.Mutex
|
||||
watchedDirs *xsync.MapOf[string, bool]
|
||||
}
|
||||
TempDir struct {
|
||||
Remote string
|
||||
Fingerprint string
|
||||
}
|
||||
)
|
||||
|
||||
// NewExecutor creates a new [Executor] and applies the given functional options
|
||||
// to it.
|
||||
func NewExecutor(opts ...ExecutorOption) *Executor {
|
||||
e := &Executor{
|
||||
Timeout: time.Second * 10,
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Logger: nil,
|
||||
Compiler: nil,
|
||||
Output: nil,
|
||||
OutputStyle: ast.Output{},
|
||||
TaskSorter: sort.AlphaNumericWithRootTasksFirst,
|
||||
UserWorkingDir: "",
|
||||
fuzzyModel: nil,
|
||||
concurrencySemaphore: nil,
|
||||
taskCallCount: map[string]*int32{},
|
||||
mkdirMutexMap: map[string]*sync.Mutex{},
|
||||
executionHashes: map[string]context.Context{},
|
||||
executionHashesMutex: sync.Mutex{},
|
||||
}
|
||||
e.Options(opts...)
|
||||
return e
|
||||
}
|
||||
|
||||
// Options loops through the given [ExecutorOption] functions and applies them
|
||||
// to the [Executor].
|
||||
func (e *Executor) Options(opts ...ExecutorOption) {
|
||||
for _, opt := range opts {
|
||||
opt.ApplyToExecutor(e)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDir sets the working directory of the [Executor]. By default, the
|
||||
// directory is set to the user's current working directory.
|
||||
func WithDir(dir string) ExecutorOption {
|
||||
return &dirOption{dir}
|
||||
}
|
||||
|
||||
type dirOption struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func (o *dirOption) ApplyToExecutor(e *Executor) {
|
||||
e.Dir = o.dir
|
||||
}
|
||||
|
||||
// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By
|
||||
// default, Task will search for one of the default Taskfiles in the given
|
||||
// directory.
|
||||
func WithEntrypoint(entrypoint string) ExecutorOption {
|
||||
return &entrypointOption{entrypoint}
|
||||
}
|
||||
|
||||
type entrypointOption struct {
|
||||
entrypoint string
|
||||
}
|
||||
|
||||
func (o *entrypointOption) ApplyToExecutor(e *Executor) {
|
||||
e.Entrypoint = o.entrypoint
|
||||
}
|
||||
|
||||
// WithTempDir sets the temporary directory that will be used by [Executor] for
|
||||
// storing temporary files like checksums and cached remote files. By default,
|
||||
// the temporary directory is set to the user's temporary directory.
|
||||
func WithTempDir(tempDir TempDir) ExecutorOption {
|
||||
return &tempDirOption{tempDir}
|
||||
}
|
||||
|
||||
type tempDirOption struct {
|
||||
tempDir TempDir
|
||||
}
|
||||
|
||||
func (o *tempDirOption) ApplyToExecutor(e *Executor) {
|
||||
e.TempDir = o.tempDir
|
||||
}
|
||||
|
||||
// WithForce ensures that the [Executor] always runs a task, even when
|
||||
// fingerprinting or prompts would normally stop it.
|
||||
func WithForce(force bool) ExecutorOption {
|
||||
return &forceOption{force}
|
||||
}
|
||||
|
||||
type forceOption struct {
|
||||
force bool
|
||||
}
|
||||
|
||||
func (o *forceOption) ApplyToExecutor(e *Executor) {
|
||||
e.Force = o.force
|
||||
}
|
||||
|
||||
// WithForceAll ensures that the [Executor] always runs all tasks (including
|
||||
// subtasks), even when fingerprinting or prompts would normally stop them.
|
||||
func WithForceAll(forceAll bool) ExecutorOption {
|
||||
return &forceAllOption{forceAll}
|
||||
}
|
||||
|
||||
type forceAllOption struct {
|
||||
forceAll bool
|
||||
}
|
||||
|
||||
func (o *forceAllOption) ApplyToExecutor(e *Executor) {
|
||||
e.ForceAll = o.forceAll
|
||||
}
|
||||
|
||||
// WithInsecure allows the [Executor] to make insecure connections when reading
|
||||
// remote taskfiles. By default, insecure connections are rejected.
|
||||
func WithInsecure(insecure bool) ExecutorOption {
|
||||
return &insecureOption{insecure}
|
||||
}
|
||||
|
||||
type insecureOption struct {
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func (o *insecureOption) ApplyToExecutor(e *Executor) {
|
||||
e.Insecure = o.insecure
|
||||
}
|
||||
|
||||
// WithDownload forces the [Executor] to download a fresh copy of the taskfile
|
||||
// from the remote source.
|
||||
func WithDownload(download bool) ExecutorOption {
|
||||
return &downloadOption{download}
|
||||
}
|
||||
|
||||
type downloadOption struct {
|
||||
download bool
|
||||
}
|
||||
|
||||
func (o *downloadOption) ApplyToExecutor(e *Executor) {
|
||||
e.Download = o.download
|
||||
}
|
||||
|
||||
// WithOffline stops the [Executor] from being able to make network connections.
|
||||
// It will still be able to read local files and cached copies of remote files.
|
||||
func WithOffline(offline bool) ExecutorOption {
|
||||
return &offlineOption{offline}
|
||||
}
|
||||
|
||||
type offlineOption struct {
|
||||
offline bool
|
||||
}
|
||||
|
||||
func (o *offlineOption) ApplyToExecutor(e *Executor) {
|
||||
e.Offline = o.offline
|
||||
}
|
||||
|
||||
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
|
||||
// default, the timeout is set to 10 seconds.
|
||||
func WithTimeout(timeout time.Duration) ExecutorOption {
|
||||
return &timeoutOption{timeout}
|
||||
}
|
||||
|
||||
type timeoutOption struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (o *timeoutOption) ApplyToExecutor(e *Executor) {
|
||||
e.Timeout = o.timeout
|
||||
}
|
||||
|
||||
// WithCacheExpiryDuration sets the duration after which the cache is considered
|
||||
// expired. By default, the cache is considered expired after 24 hours.
|
||||
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
|
||||
return &cacheExpiryDurationOption{duration: duration}
|
||||
}
|
||||
|
||||
type cacheExpiryDurationOption struct {
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
|
||||
r.CacheExpiryDuration = o.duration
|
||||
}
|
||||
|
||||
// WithWatch tells the [Executor] to keep running in the background and watch
|
||||
// for changes to the fingerprint of the tasks that are run. When changes are
|
||||
// detected, a new task run is triggered.
|
||||
func WithWatch(watch bool) ExecutorOption {
|
||||
return &watchOption{watch}
|
||||
}
|
||||
|
||||
type watchOption struct {
|
||||
watch bool
|
||||
}
|
||||
|
||||
func (o *watchOption) ApplyToExecutor(e *Executor) {
|
||||
e.Watch = o.watch
|
||||
}
|
||||
|
||||
// WithVerbose tells the [Executor] to output more information about the tasks
|
||||
// that are run.
|
||||
func WithVerbose(verbose bool) ExecutorOption {
|
||||
return &verboseOption{verbose}
|
||||
}
|
||||
|
||||
type verboseOption struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (o *verboseOption) ApplyToExecutor(e *Executor) {
|
||||
e.Verbose = o.verbose
|
||||
}
|
||||
|
||||
// WithSilent tells the [Executor] to suppress all output except for the output
|
||||
// of the tasks that are run.
|
||||
func WithSilent(silent bool) ExecutorOption {
|
||||
return &silentOption{silent}
|
||||
}
|
||||
|
||||
type silentOption struct {
|
||||
silent bool
|
||||
}
|
||||
|
||||
func (o *silentOption) ApplyToExecutor(e *Executor) {
|
||||
e.Silent = o.silent
|
||||
}
|
||||
|
||||
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
|
||||
func WithAssumeYes(assumeYes bool) ExecutorOption {
|
||||
return &assumeYesOption{assumeYes}
|
||||
}
|
||||
|
||||
type assumeYesOption struct {
|
||||
assumeYes bool
|
||||
}
|
||||
|
||||
func (o *assumeYesOption) ApplyToExecutor(e *Executor) {
|
||||
e.AssumeYes = o.assumeYes
|
||||
}
|
||||
|
||||
// WithAssumeTerm is used for testing purposes to simulate a terminal.
|
||||
func WithAssumeTerm(assumeTerm bool) ExecutorOption {
|
||||
return &assumeTermOption{assumeTerm}
|
||||
}
|
||||
|
||||
type assumeTermOption struct {
|
||||
assumeTerm bool
|
||||
}
|
||||
|
||||
func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
|
||||
e.AssumeTerm = o.assumeTerm
|
||||
}
|
||||
|
||||
// WithDry tells the [Executor] to output the commands that would be run without
|
||||
// actually running them.
|
||||
func WithDry(dry bool) ExecutorOption {
|
||||
return &dryOption{dry}
|
||||
}
|
||||
|
||||
type dryOption struct {
|
||||
dry bool
|
||||
}
|
||||
|
||||
func (o *dryOption) ApplyToExecutor(e *Executor) {
|
||||
e.Dry = o.dry
|
||||
}
|
||||
|
||||
// WithSummary tells the [Executor] to output a summary of the given tasks
|
||||
// instead of running them.
|
||||
func WithSummary(summary bool) ExecutorOption {
|
||||
return &summaryOption{summary}
|
||||
}
|
||||
|
||||
type summaryOption struct {
|
||||
summary bool
|
||||
}
|
||||
|
||||
func (o *summaryOption) ApplyToExecutor(e *Executor) {
|
||||
e.Summary = o.summary
|
||||
}
|
||||
|
||||
// WithParallel tells the [Executor] to run tasks given in the same call in
|
||||
// parallel.
|
||||
func WithParallel(parallel bool) ExecutorOption {
|
||||
return ¶llelOption{parallel}
|
||||
}
|
||||
|
||||
type parallelOption struct {
|
||||
parallel bool
|
||||
}
|
||||
|
||||
func (o *parallelOption) ApplyToExecutor(e *Executor) {
|
||||
e.Parallel = o.parallel
|
||||
}
|
||||
|
||||
// WithColor tells the [Executor] whether or not to output using colorized
|
||||
// strings.
|
||||
func WithColor(color bool) ExecutorOption {
|
||||
return &colorOption{color}
|
||||
}
|
||||
|
||||
type colorOption struct {
|
||||
color bool
|
||||
}
|
||||
|
||||
func (o *colorOption) ApplyToExecutor(e *Executor) {
|
||||
e.Color = o.color
|
||||
}
|
||||
|
||||
// WithConcurrency sets the maximum number of tasks that the [Executor] can run
|
||||
// in parallel.
|
||||
func WithConcurrency(concurrency int) ExecutorOption {
|
||||
return &concurrencyOption{concurrency}
|
||||
}
|
||||
|
||||
type concurrencyOption struct {
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func (o *concurrencyOption) ApplyToExecutor(e *Executor) {
|
||||
e.Concurrency = o.concurrency
|
||||
}
|
||||
|
||||
// WithInterval sets the interval at which the [Executor] will wait for
|
||||
// duplicated events before running a task.
|
||||
func WithInterval(interval time.Duration) ExecutorOption {
|
||||
return &intervalOption{interval}
|
||||
}
|
||||
|
||||
type intervalOption struct {
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func (o *intervalOption) ApplyToExecutor(e *Executor) {
|
||||
e.Interval = o.interval
|
||||
}
|
||||
|
||||
// WithOutputStyle sets the output style of the [Executor]. By default, the
|
||||
// output style is set to the style defined in the Taskfile.
|
||||
func WithOutputStyle(outputStyle ast.Output) ExecutorOption {
|
||||
return &outputStyleOption{outputStyle}
|
||||
}
|
||||
|
||||
type outputStyleOption struct {
|
||||
outputStyle ast.Output
|
||||
}
|
||||
|
||||
func (o *outputStyleOption) ApplyToExecutor(e *Executor) {
|
||||
e.OutputStyle = o.outputStyle
|
||||
}
|
||||
|
||||
// WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By
|
||||
// default, the sorter is set to sort tasks alphabetically, but with tasks with
|
||||
// no namespace (in the root Taskfile) first.
|
||||
func WithTaskSorter(sorter sort.Sorter) ExecutorOption {
|
||||
return &taskSorterOption{sorter}
|
||||
}
|
||||
|
||||
type taskSorterOption struct {
|
||||
sorter sort.Sorter
|
||||
}
|
||||
|
||||
func (o *taskSorterOption) ApplyToExecutor(e *Executor) {
|
||||
e.TaskSorter = o.sorter
|
||||
}
|
||||
|
||||
// WithStdin sets the [Executor]'s standard input [io.Reader].
|
||||
func WithStdin(stdin io.Reader) ExecutorOption {
|
||||
return &stdinOption{stdin}
|
||||
}
|
||||
|
||||
type stdinOption struct {
|
||||
stdin io.Reader
|
||||
}
|
||||
|
||||
func (o *stdinOption) ApplyToExecutor(e *Executor) {
|
||||
e.Stdin = o.stdin
|
||||
}
|
||||
|
||||
// WithStdout sets the [Executor]'s standard output [io.Writer].
|
||||
func WithStdout(stdout io.Writer) ExecutorOption {
|
||||
return &stdoutOption{stdout}
|
||||
}
|
||||
|
||||
type stdoutOption struct {
|
||||
stdout io.Writer
|
||||
}
|
||||
|
||||
func (o *stdoutOption) ApplyToExecutor(e *Executor) {
|
||||
e.Stdout = o.stdout
|
||||
}
|
||||
|
||||
// WithStderr sets the [Executor]'s standard error [io.Writer].
|
||||
func WithStderr(stderr io.Writer) ExecutorOption {
|
||||
return &stderrOption{stderr}
|
||||
}
|
||||
|
||||
type stderrOption struct {
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
func (o *stderrOption) ApplyToExecutor(e *Executor) {
|
||||
e.Stderr = o.stderr
|
||||
}
|
||||
|
||||
// WithIO sets the [Executor]'s standard input, output, and error to the same
|
||||
// [io.ReadWriter].
|
||||
func WithIO(rw io.ReadWriter) ExecutorOption {
|
||||
return &ioOption{rw}
|
||||
}
|
||||
|
||||
type ioOption struct {
|
||||
rw io.ReadWriter
|
||||
}
|
||||
|
||||
func (o *ioOption) ApplyToExecutor(e *Executor) {
|
||||
e.Stdin = o.rw
|
||||
e.Stdout = o.rw
|
||||
e.Stderr = o.rw
|
||||
}
|
||||
|
||||
// WithVersionCheck tells the [Executor] whether or not to check the version of
|
||||
func WithVersionCheck(enableVersionCheck bool) ExecutorOption {
|
||||
return &versionCheckOption{enableVersionCheck}
|
||||
}
|
||||
|
||||
type versionCheckOption struct {
|
||||
enableVersionCheck bool
|
||||
}
|
||||
|
||||
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
|
||||
e.EnableVersionCheck = o.enableVersionCheck
|
||||
}
|
||||
989
executor_test.go
Normal file
989
executor_test.go
Normal file
@@ -0,0 +1,989 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/sebdah/goldie/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type (
|
||||
// A ExecutorTestOption is a function that configures an [ExecutorTest].
|
||||
ExecutorTestOption interface {
|
||||
applyToExecutorTest(*ExecutorTest)
|
||||
}
|
||||
// A ExecutorTest is a test wrapper around a [task.Executor] to make it easy
|
||||
// to write tests for tasks. See [NewExecutorTest] for information on
|
||||
// creating and running ExecutorTests. These tests use fixture files to
|
||||
// assert whether the result of a task is correct. If Task's behavior has
|
||||
// been changed, the fixture files can be updated by running `task
|
||||
// gen:fixtures`.
|
||||
ExecutorTest struct {
|
||||
TaskTest
|
||||
task string
|
||||
vars map[string]any
|
||||
input string
|
||||
executorOpts []task.ExecutorOption
|
||||
wantSetupError bool
|
||||
wantRunError bool
|
||||
wantStatusError bool
|
||||
}
|
||||
)
|
||||
|
||||
// NewExecutorTest sets up a new [task.Executor] with the given options and runs
|
||||
// a task with the given [ExecutorTestOption]s. The output of the task is
|
||||
// written to a set of fixture files depending on the configuration of the test.
|
||||
func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
|
||||
t.Helper()
|
||||
tt := &ExecutorTest{
|
||||
task: "default",
|
||||
vars: map[string]any{},
|
||||
TaskTest: TaskTest{
|
||||
experiments: map[*experiments.Experiment]int{},
|
||||
fixtureTemplateData: map[string]any{},
|
||||
},
|
||||
}
|
||||
// Apply the functional options
|
||||
for _, opt := range opts {
|
||||
opt.applyToExecutorTest(tt)
|
||||
}
|
||||
// Enable any experiments that have been set
|
||||
for x, v := range tt.experiments {
|
||||
prev := *x
|
||||
*x = experiments.Experiment{
|
||||
Name: prev.Name,
|
||||
AllowedValues: []int{v},
|
||||
Value: v,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
*x = prev
|
||||
})
|
||||
}
|
||||
tt.run(t)
|
||||
}
|
||||
|
||||
// Functional options
|
||||
|
||||
// WithInput tells the test to create a reader with the given input. This can be
|
||||
// used to simulate user input when a task requires it.
|
||||
func WithInput(input string) ExecutorTestOption {
|
||||
return &inputTestOption{input}
|
||||
}
|
||||
|
||||
type inputTestOption struct {
|
||||
input string
|
||||
}
|
||||
|
||||
func (opt *inputTestOption) applyToExecutorTest(t *ExecutorTest) {
|
||||
t.input = opt.input
|
||||
}
|
||||
|
||||
// WithRunError tells the test to expect an error during the run phase of the
|
||||
// task execution. A fixture will be created with the output of any errors.
|
||||
func WithRunError() ExecutorTestOption {
|
||||
return &runErrorTestOption{}
|
||||
}
|
||||
|
||||
type runErrorTestOption struct{}
|
||||
|
||||
func (opt *runErrorTestOption) applyToExecutorTest(t *ExecutorTest) {
|
||||
t.wantRunError = true
|
||||
}
|
||||
|
||||
// WithStatusError tells the test to make an additional call to
|
||||
// [task.Executor.Status] after the task has been run. A fixture will be created
|
||||
// with the output of any errors.
|
||||
func WithStatusError() ExecutorTestOption {
|
||||
return &statusErrorTestOption{}
|
||||
}
|
||||
|
||||
type statusErrorTestOption struct{}
|
||||
|
||||
func (opt *statusErrorTestOption) applyToExecutorTest(t *ExecutorTest) {
|
||||
t.wantStatusError = true
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// writeFixtureErrRun is a wrapper for writing the output of an error during the
|
||||
// run phase of the task to a fixture file.
|
||||
func (tt *ExecutorTest) writeFixtureErrRun(
|
||||
t *testing.T,
|
||||
g *goldie.Goldie,
|
||||
err error,
|
||||
) {
|
||||
t.Helper()
|
||||
tt.writeFixture(t, g, "err-run", []byte(err.Error()))
|
||||
}
|
||||
|
||||
// writeFixtureStatus is a wrapper for writing the output of an error when
|
||||
// making an additional call to [task.Executor.Status] to a fixture file.
|
||||
func (tt *ExecutorTest) writeFixtureStatus(
|
||||
t *testing.T,
|
||||
g *goldie.Goldie,
|
||||
status string,
|
||||
) {
|
||||
t.Helper()
|
||||
tt.writeFixture(t, g, "err-status", []byte(status))
|
||||
}
|
||||
|
||||
// run is the main function for running the test. It sets up the task executor,
|
||||
// runs the task, and writes the output to a fixture file.
|
||||
func (tt *ExecutorTest) run(t *testing.T) {
|
||||
t.Helper()
|
||||
f := func(t *testing.T) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
|
||||
opts := append(
|
||||
tt.executorOpts,
|
||||
task.WithStdout(&buf),
|
||||
task.WithStderr(&buf),
|
||||
)
|
||||
|
||||
// If the test has input, create a reader for it and add it to the
|
||||
// executor options
|
||||
if tt.input != "" {
|
||||
var reader bytes.Buffer
|
||||
reader.WriteString(tt.input)
|
||||
opts = append(opts, task.WithStdin(&reader))
|
||||
}
|
||||
|
||||
// Set up the task executor
|
||||
e := task.NewExecutor(opts...)
|
||||
|
||||
// Create a golden fixture file for the output
|
||||
g := goldie.New(t,
|
||||
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
|
||||
)
|
||||
|
||||
// Call setup and check for errors
|
||||
if err := e.Setup(); tt.wantSetupError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrSetup(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create the task call
|
||||
vars := ast.NewVars()
|
||||
for key, value := range tt.vars {
|
||||
vars.Set(key, ast.Var{Value: value})
|
||||
}
|
||||
call := &task.Call{
|
||||
Task: tt.task,
|
||||
Vars: vars,
|
||||
}
|
||||
|
||||
// Run the task and check for errors
|
||||
ctx := t.Context()
|
||||
if err := e.Run(ctx, call); tt.wantRunError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrRun(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// If the status flag is set, run the status check
|
||||
if tt.wantStatusError {
|
||||
if err := e.Status(ctx, call); err != nil {
|
||||
tt.writeFixtureStatus(t, g, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
}
|
||||
|
||||
// Run the test (with a name if it has one)
|
||||
if tt.name != "" {
|
||||
t.Run(tt.name, f)
|
||||
} else {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/empty_task"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func TestEmptyTaskfile(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/empty_taskfile"),
|
||||
),
|
||||
WithSetupError(),
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
t.Setenv("QUX", "from_os")
|
||||
NewExecutorTest(t,
|
||||
WithName("env precedence disabled"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/env"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("env precedence enabled"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/env"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithExperiment(&experiments.EnvPrecedence, 1),
|
||||
)
|
||||
}
|
||||
|
||||
func TestVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/vars"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func TestRequires(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithName("required var missing"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("missing-var"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("required var ok"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("missing-var"),
|
||||
WithVar("FOO", "bar"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("fails validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var"),
|
||||
WithVar("ENV", "dev"),
|
||||
WithVar("FOO", "bar"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("passes validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var"),
|
||||
WithVar("FOO", "one"),
|
||||
WithVar("ENV", "dev"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("required var missing + fails validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("required var missing + fails validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var-dynamic"),
|
||||
WithVar("FOO", "one"),
|
||||
WithVar("ENV", "dev"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("require before compile"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("require-before-compile"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("var defined in task"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("var-defined-in-task"),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: mock fs
|
||||
func TestSpecialVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const dir = "testdata/special_vars"
|
||||
const subdir = "testdata/special_vars/subdir"
|
||||
|
||||
tests := []string{
|
||||
// Root
|
||||
"print-task",
|
||||
"print-root-dir",
|
||||
"print-taskfile",
|
||||
"print-taskfile-dir",
|
||||
"print-task-dir",
|
||||
// Included
|
||||
"included:print-task",
|
||||
"included:print-root-dir",
|
||||
"included:print-taskfile",
|
||||
"included:print-taskfile-dir",
|
||||
}
|
||||
|
||||
for _, dir := range []string{dir, subdir} {
|
||||
for _, test := range tests {
|
||||
NewExecutorTest(t,
|
||||
WithName(fmt.Sprintf("%s-%s", dir, test)),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
task.WithVersionCheck(true),
|
||||
),
|
||||
WithTask(test),
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/concurrency"),
|
||||
task.WithConcurrency(1),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
)
|
||||
}
|
||||
|
||||
func TestParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/params"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/deps"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: mock fs
|
||||
func TestStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const dir = "testdata/status"
|
||||
|
||||
files := []string{
|
||||
"foo.txt",
|
||||
"bar.txt",
|
||||
"baz.txt",
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
path := filepathext.SmartJoin(dir, f)
|
||||
_ = os.Remove(path)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Errorf("File should not exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// gen-foo creates foo.txt, and will always fail it's status check.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-foo 1 silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-foo"),
|
||||
)
|
||||
// gen-foo creates bar.txt, and will pass its status-check the 3. time it
|
||||
// is run. It creates bar.txt, but also lists it as its source. So, the checksum
|
||||
// for the file won't match before after the second run as we the file
|
||||
// only exists after the first run.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 1 silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
// gen-silent-baz is marked as being silent, and should only produce output
|
||||
// if e.Verbose is set to true.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-baz silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-silent-baz"),
|
||||
)
|
||||
|
||||
for _, f := range files {
|
||||
if _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil {
|
||||
t.Errorf("File should exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run gen-bar a second time to produce a checksum file that matches bar.txt
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 2 silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
// Run gen-bar a third time, to make sure we've triggered the status check.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 3 silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
|
||||
// Now, let's remove source file, and run the task again to to prepare
|
||||
// for the next test.
|
||||
err := os.Remove(filepathext.SmartJoin(dir, "bar.txt"))
|
||||
require.NoError(t, err)
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 4 silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
// all: not up-to-date
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-foo 2"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-foo"),
|
||||
)
|
||||
// status: not up-to-date
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-foo 3"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-foo"),
|
||||
)
|
||||
// sources: not up-to-date
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 5"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
// all: up-to-date
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-bar 6"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-bar"),
|
||||
)
|
||||
// sources: not up-to-date, no output produced.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-baz 2"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-silent-baz"),
|
||||
)
|
||||
// up-to-date, no output produced
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-baz 3"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("gen-silent-baz"),
|
||||
)
|
||||
// up-to-date, output produced due to Verbose mode.
|
||||
NewExecutorTest(t,
|
||||
WithName("run gen-baz 4 verbose"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
task.WithVerbose(true),
|
||||
),
|
||||
WithTask("gen-silent-baz"),
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestPrecondition(t *testing.T) {
|
||||
t.Parallel()
|
||||
const dir = "testdata/precondition"
|
||||
NewExecutorTest(t,
|
||||
WithName("a precondition has been met"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("a precondition was not met"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("impossible"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("precondition in dependency fails the task"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("depends_on_impossible"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("precondition in cmd fails the task"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(dir),
|
||||
),
|
||||
WithTask("executes_failing_task_as_cmd"),
|
||||
WithRunError(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestAlias(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("alias"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/alias"),
|
||||
),
|
||||
WithTask("f"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("duplicate alias"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/alias"),
|
||||
),
|
||||
WithTask("x"),
|
||||
WithRunError(),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("alias summary"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/alias"),
|
||||
task.WithSummary(true),
|
||||
),
|
||||
WithTask("f"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestLabel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("up to date"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_uptodate"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("summary"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_summary"),
|
||||
task.WithSummary(true),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("status"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_status"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
WithStatusError(),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("var"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_var"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("label in summary"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_summary"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestPromptInSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantError bool
|
||||
}{
|
||||
{"test short approval", "y\n", false},
|
||||
{"test long approval", "yes\n", false},
|
||||
{"test uppercase approval", "Y\n", false},
|
||||
{"test stops task", "n\n", true},
|
||||
{"test junk value stops task", "foobar\n", true},
|
||||
{"test Enter stops task", "\n", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
opts := []ExecutorTestOption{
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prompt"),
|
||||
task.WithAssumeTerm(true),
|
||||
),
|
||||
WithTask("foo"),
|
||||
WithInput(test.input),
|
||||
}
|
||||
if test.wantError {
|
||||
opts = append(opts, WithRunError())
|
||||
}
|
||||
NewExecutorTest(t, opts...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptWithIndirectTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prompt"),
|
||||
task.WithAssumeTerm(true),
|
||||
),
|
||||
WithTask("bar"),
|
||||
WithInput("y\n"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestPromptAssumeYes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("--yes flag should skip prompt"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prompt"),
|
||||
task.WithAssumeTerm(true),
|
||||
task.WithAssumeYes(true),
|
||||
),
|
||||
WithTask("foo"),
|
||||
WithInput("\n"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("task should raise errors.TaskCancelledError"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prompt"),
|
||||
task.WithAssumeTerm(true),
|
||||
),
|
||||
WithTask("foo"),
|
||||
WithInput("\n"),
|
||||
WithRunError(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestForCmds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "loop-explicit"},
|
||||
{name: "loop-matrix"},
|
||||
{name: "loop-matrix-ref"},
|
||||
{
|
||||
name: "loop-matrix-ref-error",
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "loop-sources"},
|
||||
{name: "loop-sources-glob"},
|
||||
{name: "loop-generates"},
|
||||
{name: "loop-generates-glob"},
|
||||
{name: "loop-vars"},
|
||||
{name: "loop-vars-sh"},
|
||||
{name: "loop-task"},
|
||||
{name: "loop-task-as"},
|
||||
{name: "loop-different-tasks"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
opts := []ExecutorTestOption{
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/for/cmds"),
|
||||
task.WithSilent(true),
|
||||
task.WithForce(true),
|
||||
),
|
||||
WithTask(test.name),
|
||||
WithFixtureTemplating(),
|
||||
}
|
||||
if test.wantErr {
|
||||
opts = append(opts, WithRunError())
|
||||
}
|
||||
NewExecutorTest(t, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "loop-explicit"},
|
||||
{name: "loop-matrix"},
|
||||
{name: "loop-matrix-ref"},
|
||||
{
|
||||
name: "loop-matrix-ref-error",
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "loop-sources"},
|
||||
{name: "loop-sources-glob"},
|
||||
{name: "loop-generates"},
|
||||
{name: "loop-generates-glob"},
|
||||
{name: "loop-vars"},
|
||||
{name: "loop-vars-sh"},
|
||||
{name: "loop-task"},
|
||||
{name: "loop-task-as"},
|
||||
{name: "loop-different-tasks"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
opts := []ExecutorTestOption{
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/for/deps"),
|
||||
task.WithSilent(true),
|
||||
task.WithForce(true),
|
||||
// Force output of each dep to be grouped together to prevent interleaving
|
||||
task.WithOutputStyle(ast.Output{Name: "group"}),
|
||||
),
|
||||
WithTask(test.name),
|
||||
WithFixtureTemplating(),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
}
|
||||
if test.wantErr {
|
||||
opts = append(opts, WithRunError())
|
||||
}
|
||||
NewExecutorTest(t, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReference(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
call string
|
||||
}{
|
||||
{
|
||||
name: "reference in command",
|
||||
call: "ref-cmd",
|
||||
},
|
||||
{
|
||||
name: "reference in dependency",
|
||||
call: "ref-dep",
|
||||
},
|
||||
{
|
||||
name: "reference using templating resolver",
|
||||
call: "ref-resolver",
|
||||
},
|
||||
{
|
||||
name: "reference using templating resolver and dynamic var",
|
||||
call: "ref-resolver-sh",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
NewExecutorTest(t,
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/var_references"),
|
||||
task.WithSilent(true),
|
||||
task.WithForce(true),
|
||||
),
|
||||
WithTask(cmp.Or(test.call, "default")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarInheritance(t *testing.T) {
|
||||
enableExperimentForTest(t, &experiments.EnvPrecedence, 1)
|
||||
tests := []struct {
|
||||
name string
|
||||
call string
|
||||
}{
|
||||
{name: "shell"},
|
||||
{name: "entrypoint-global-dotenv"},
|
||||
{name: "entrypoint-global-vars"},
|
||||
// We can't send env vars to a called task, so the env var is not overridden
|
||||
{name: "entrypoint-task-call-vars"},
|
||||
// Dotenv doesn't set variables
|
||||
{name: "entrypoint-task-call-dotenv"},
|
||||
{name: "entrypoint-task-call-task-vars"},
|
||||
// Dotenv doesn't set variables
|
||||
{name: "entrypoint-task-dotenv"},
|
||||
{name: "entrypoint-task-vars"},
|
||||
// {
|
||||
// // Dotenv not currently allowed in included taskfiles
|
||||
// name: "included-global-dotenv",
|
||||
// want: "included-global-dotenv\nincluded-global-dotenv\n",
|
||||
// },
|
||||
{
|
||||
name: "included-global-vars",
|
||||
call: "included",
|
||||
},
|
||||
{
|
||||
// We can't send env vars to a called task, so the env var is not overridden
|
||||
name: "included-task-call-vars",
|
||||
call: "included",
|
||||
},
|
||||
{
|
||||
// Dotenv doesn't set variables
|
||||
// Dotenv not currently allowed in included taskfiles (but doesn't error in a task)
|
||||
name: "included-task-call-dotenv",
|
||||
call: "included",
|
||||
},
|
||||
{
|
||||
name: "included-task-call-task-vars",
|
||||
call: "included",
|
||||
},
|
||||
{
|
||||
// Dotenv doesn't set variables
|
||||
// Somehow dotenv is working here!
|
||||
name: "included-task-dotenv",
|
||||
call: "included",
|
||||
},
|
||||
{
|
||||
name: "included-task-vars",
|
||||
call: "included",
|
||||
},
|
||||
}
|
||||
|
||||
t.Setenv("VAR", "shell")
|
||||
t.Setenv("ENV", "shell")
|
||||
for _, test := range tests {
|
||||
NewExecutorTest(t,
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
|
||||
task.WithSilent(true),
|
||||
task.WithForce(true),
|
||||
),
|
||||
WithTask(cmp.Or(test.call, "default")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyModel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("fuzzy"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/fuzzy"),
|
||||
),
|
||||
WithTask("instal"),
|
||||
WithRunError(),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("not-fuzzy"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/fuzzy"),
|
||||
),
|
||||
WithTask("install"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("intern"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/fuzzy"),
|
||||
),
|
||||
WithTask("intern"),
|
||||
WithRunError(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestIncludeChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("correct"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/includes_checksum/correct"),
|
||||
),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("incorrect"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/includes_checksum/incorrect"),
|
||||
),
|
||||
WithSetupError(),
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
35
experiments/errors.go
Normal file
35
experiments/errors.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/slicesext"
|
||||
)
|
||||
|
||||
type InvalidValueError struct {
|
||||
Name string
|
||||
AllowedValues []int
|
||||
Value int
|
||||
}
|
||||
|
||||
func (err InvalidValueError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"task: Experiment %q has an invalid value %q (allowed values: %s)",
|
||||
err.Name,
|
||||
err.Value,
|
||||
strings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), ", "),
|
||||
)
|
||||
}
|
||||
|
||||
type InactiveError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (err InactiveError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"task: Experiment %q is inactive and cannot be enabled",
|
||||
err.Name,
|
||||
)
|
||||
}
|
||||
67
experiments/experiment.go
Normal file
67
experiments/experiment.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
|
||||
type Experiment struct {
|
||||
Name string // The name of the experiment.
|
||||
AllowedValues []int // The values that can enable this experiment.
|
||||
Value int // The version of the experiment that is enabled.
|
||||
}
|
||||
|
||||
// New creates a new experiment with the given name and sets the values that can
|
||||
// enable it.
|
||||
func New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment {
|
||||
var value int
|
||||
if config != nil {
|
||||
value = config.Experiments[xName]
|
||||
}
|
||||
|
||||
if value == 0 {
|
||||
value, _ = strconv.Atoi(getEnv(xName))
|
||||
}
|
||||
|
||||
x := Experiment{
|
||||
Name: xName,
|
||||
AllowedValues: allowedValues,
|
||||
Value: value,
|
||||
}
|
||||
xList = append(xList, x)
|
||||
return x
|
||||
}
|
||||
|
||||
func (x Experiment) Enabled() bool {
|
||||
return slices.Contains(x.AllowedValues, x.Value)
|
||||
}
|
||||
|
||||
func (x Experiment) Active() bool {
|
||||
return len(x.AllowedValues) > 0
|
||||
}
|
||||
|
||||
func (x Experiment) Valid() error {
|
||||
if !x.Active() && x.Value != 0 {
|
||||
return &InactiveError{
|
||||
Name: x.Name,
|
||||
}
|
||||
}
|
||||
if !x.Enabled() && x.Value != 0 {
|
||||
return &InvalidValueError{
|
||||
Name: x.Name,
|
||||
AllowedValues: x.AllowedValues,
|
||||
Value: x.Value,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x Experiment) String() string {
|
||||
if x.Enabled() {
|
||||
return fmt.Sprintf("on (%d)", x.Value)
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
140
experiments/experiment_test.go
Normal file
140
experiments/experiment_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package experiments_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
const (
|
||||
exampleExperiment = "EXAMPLE"
|
||||
exampleExperimentEnv = "TASK_X_EXAMPLE"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
config *ast.TaskRC
|
||||
allowedValues []int
|
||||
env int
|
||||
wantEnabled bool
|
||||
wantActive bool
|
||||
wantValid error
|
||||
wantValue int
|
||||
}{
|
||||
{
|
||||
name: `[] allowed, env=""`,
|
||||
wantEnabled: false,
|
||||
wantActive: false,
|
||||
},
|
||||
{
|
||||
name: `[] allowed, env="1"`,
|
||||
env: 1,
|
||||
wantEnabled: false,
|
||||
wantActive: false,
|
||||
wantValid: &experiments.InactiveError{
|
||||
Name: exampleExperiment,
|
||||
},
|
||||
wantValue: 1,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, env=""`,
|
||||
allowedValues: []int{1},
|
||||
wantEnabled: false,
|
||||
wantActive: true,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, env="1"`,
|
||||
allowedValues: []int{1},
|
||||
env: 1,
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
wantValue: 1,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, env="2"`,
|
||||
allowedValues: []int{1},
|
||||
env: 2,
|
||||
wantEnabled: false,
|
||||
wantActive: true,
|
||||
wantValid: &experiments.InvalidValueError{
|
||||
Name: exampleExperiment,
|
||||
AllowedValues: []int{1},
|
||||
Value: 2,
|
||||
},
|
||||
wantValue: 2,
|
||||
},
|
||||
{
|
||||
name: `[1, 2] allowed, env="1"`,
|
||||
allowedValues: []int{1, 2},
|
||||
env: 1,
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
wantValue: 1,
|
||||
},
|
||||
{
|
||||
name: `[1, 2] allowed, env="1"`,
|
||||
allowedValues: []int{1, 2},
|
||||
env: 2,
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
wantValue: 2,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, config="1"`,
|
||||
config: &ast.TaskRC{
|
||||
Experiments: map[string]int{
|
||||
exampleExperiment: 1,
|
||||
},
|
||||
},
|
||||
allowedValues: []int{1},
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
wantValue: 1,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, config="2"`,
|
||||
config: &ast.TaskRC{
|
||||
Experiments: map[string]int{
|
||||
exampleExperiment: 2,
|
||||
},
|
||||
},
|
||||
allowedValues: []int{1},
|
||||
wantEnabled: false,
|
||||
wantActive: true,
|
||||
wantValid: &experiments.InvalidValueError{
|
||||
Name: exampleExperiment,
|
||||
AllowedValues: []int{1},
|
||||
Value: 2,
|
||||
},
|
||||
wantValue: 2,
|
||||
},
|
||||
{
|
||||
name: `[1, 2] allowed, env="1", config="2"`,
|
||||
config: &ast.TaskRC{
|
||||
Experiments: map[string]int{
|
||||
exampleExperiment: 2,
|
||||
},
|
||||
},
|
||||
allowedValues: []int{1, 2},
|
||||
env: 1,
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
wantValue: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.env))
|
||||
x := experiments.New(exampleExperiment, tt.config, tt.allowedValues...)
|
||||
assert.Equal(t, exampleExperiment, x.Name)
|
||||
assert.Equal(t, tt.wantEnabled, x.Enabled())
|
||||
assert.Equal(t, tt.wantActive, x.Active())
|
||||
assert.Equal(t, tt.wantValid, x.Valid())
|
||||
assert.Equal(t, tt.wantValue, x.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
91
experiments/experiments.go
Normal file
91
experiments/experiments.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/go-task/task/v3/taskrc"
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
|
||||
const envPrefix = "TASK_X_"
|
||||
|
||||
// Active experiments.
|
||||
var (
|
||||
GentleForce Experiment
|
||||
RemoteTaskfiles Experiment
|
||||
EnvPrecedence Experiment
|
||||
)
|
||||
|
||||
// Inactive experiments. These are experiments that cannot be enabled, but are
|
||||
// preserved for error handling.
|
||||
var (
|
||||
AnyVariables Experiment
|
||||
MapVariables Experiment
|
||||
)
|
||||
|
||||
// An internal list of all the initialized experiments used for iterating.
|
||||
var xList []Experiment
|
||||
|
||||
func Parse(dir string) {
|
||||
config, _ := taskrc.GetConfig(dir)
|
||||
|
||||
ParseWithConfig(dir, config)
|
||||
}
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
// Read any .env files
|
||||
readDotEnv(dir)
|
||||
|
||||
// Initialize the experiments
|
||||
GentleForce = New("GENTLE_FORCE", config, 1)
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)
|
||||
EnvPrecedence = New("ENV_PRECEDENCE", config, 1)
|
||||
AnyVariables = New("ANY_VARIABLES", config)
|
||||
MapVariables = New("MAP_VARIABLES", config)
|
||||
}
|
||||
|
||||
// Validate checks if any experiments have been enabled while being inactive.
|
||||
// If one is found, the function returns an error.
|
||||
func Validate() error {
|
||||
for _, x := range List() {
|
||||
if err := x.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func List() []Experiment {
|
||||
return xList
|
||||
}
|
||||
|
||||
func getEnv(xName string) string {
|
||||
envName := fmt.Sprintf("%s%s", envPrefix, xName)
|
||||
return os.Getenv(envName)
|
||||
}
|
||||
|
||||
func getFilePath(filename, dir string) string {
|
||||
if dir != "" {
|
||||
return filepath.Join(dir, filename)
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func readDotEnv(dir string) {
|
||||
env, err := godotenv.Read(getFilePath(".env", dir))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If the env var is an experiment, set it.
|
||||
for key, value := range env {
|
||||
if strings.HasPrefix(key, envPrefix) {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
235
formatter_test.go
Normal file
235
formatter_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/sebdah/goldie/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type (
|
||||
// A FormatterTestOption is a function that configures an [FormatterTest].
|
||||
FormatterTestOption interface {
|
||||
applyToFormatterTest(*FormatterTest)
|
||||
}
|
||||
// A FormatterTest is a test wrapper around a [task.Executor] to make it
|
||||
// easy to write tests for the task formatter. See [NewFormatterTest] for
|
||||
// information on creating and running FormatterTests. These tests use
|
||||
// fixture files to assert whether the result of the output is correct. If
|
||||
// Task's behavior has been changed, the fixture files can be updated by
|
||||
// running `task gen:fixtures`.
|
||||
FormatterTest struct {
|
||||
TaskTest
|
||||
task string
|
||||
vars map[string]any
|
||||
executorOpts []task.ExecutorOption
|
||||
listOptions task.ListOptions
|
||||
wantSetupError bool
|
||||
wantListError bool
|
||||
}
|
||||
)
|
||||
|
||||
// NewFormatterTest sets up a new [task.Executor] with the given options and
|
||||
// runs a task with the given [FormatterTestOption]s. The output of the task is
|
||||
// written to a set of fixture files depending on the configuration of the test.
|
||||
func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
|
||||
t.Helper()
|
||||
tt := &FormatterTest{
|
||||
task: "default",
|
||||
vars: map[string]any{},
|
||||
TaskTest: TaskTest{
|
||||
experiments: map[*experiments.Experiment]int{},
|
||||
fixtureTemplateData: map[string]any{},
|
||||
},
|
||||
}
|
||||
// Apply the functional options
|
||||
for _, opt := range opts {
|
||||
opt.applyToFormatterTest(tt)
|
||||
}
|
||||
// Enable any experiments that have been set
|
||||
for x, v := range tt.experiments {
|
||||
prev := *x
|
||||
*x = experiments.Experiment{
|
||||
Name: prev.Name,
|
||||
AllowedValues: []int{v},
|
||||
Value: v,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
*x = prev
|
||||
})
|
||||
}
|
||||
tt.run(t)
|
||||
}
|
||||
|
||||
// Functional options
|
||||
|
||||
// WithListOptions sets the list options for the formatter.
|
||||
func WithListOptions(opts task.ListOptions) FormatterTestOption {
|
||||
return &listOptionsTestOption{opts}
|
||||
}
|
||||
|
||||
type listOptionsTestOption struct {
|
||||
listOptions task.ListOptions
|
||||
}
|
||||
|
||||
func (opt *listOptionsTestOption) applyToFormatterTest(t *FormatterTest) {
|
||||
t.listOptions = opt.listOptions
|
||||
}
|
||||
|
||||
// WithListError tells the test to expect an error when running the formatter.
|
||||
// A fixture will be created with the output of any errors.
|
||||
func WithListError() FormatterTestOption {
|
||||
return &listErrorTestOption{}
|
||||
}
|
||||
|
||||
type listErrorTestOption struct{}
|
||||
|
||||
func (opt *listErrorTestOption) applyToFormatterTest(t *FormatterTest) {
|
||||
t.wantListError = true
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// writeFixtureErrList is a wrapper for writing the output of an error when
|
||||
// running the formatter to a fixture file.
|
||||
func (tt *FormatterTest) writeFixtureErrList(
|
||||
t *testing.T,
|
||||
g *goldie.Goldie,
|
||||
err error,
|
||||
) {
|
||||
t.Helper()
|
||||
tt.writeFixture(t, g, "err-list", []byte(err.Error()))
|
||||
}
|
||||
|
||||
// run is the main function for running the test. It sets up the task executor,
|
||||
// runs the task, and writes the output to a fixture file.
|
||||
func (tt *FormatterTest) run(t *testing.T) {
|
||||
t.Helper()
|
||||
f := func(t *testing.T) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
|
||||
opts := append(
|
||||
tt.executorOpts,
|
||||
task.WithStdout(&buf),
|
||||
task.WithStderr(&buf),
|
||||
)
|
||||
|
||||
// Set up the task executor
|
||||
e := task.NewExecutor(opts...)
|
||||
|
||||
// Create a golden fixture file for the output
|
||||
g := goldie.New(t,
|
||||
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
|
||||
)
|
||||
|
||||
// Call setup and check for errors
|
||||
if err := e.Setup(); tt.wantSetupError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrSetup(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create the task call
|
||||
vars := ast.NewVars()
|
||||
for key, value := range tt.vars {
|
||||
vars.Set(key, ast.Var{Value: value})
|
||||
}
|
||||
|
||||
// Run the formatter and check for errors
|
||||
if _, err := e.ListTasks(tt.listOptions); tt.wantListError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrList(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
}
|
||||
|
||||
// Run the test (with a name if it has one)
|
||||
if tt.name != "" {
|
||||
t.Run(tt.name, f)
|
||||
} else {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoLabelInList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewFormatterTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_list"),
|
||||
),
|
||||
WithListOptions(task.ListOptions{
|
||||
ListOnlyTasksWithDescriptions: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// task -al case 1: listAll list all tasks
|
||||
func TestListAllShowsNoDesc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewFormatterTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/list_mixed_desc"),
|
||||
),
|
||||
WithListOptions(task.ListOptions{
|
||||
ListAllTasks: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// task -al case 2: !listAll list some tasks (only those with desc)
|
||||
func TestListCanListDescOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewFormatterTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/list_mixed_desc"),
|
||||
),
|
||||
WithListOptions(task.ListOptions{
|
||||
ListOnlyTasksWithDescriptions: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func TestListDescInterpolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewFormatterTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/list_desc_interpolation"),
|
||||
),
|
||||
WithListOptions(task.ListOptions{
|
||||
ListOnlyTasksWithDescriptions: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func TestJsonListFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewFormatterTest(t,
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/json_list_format"),
|
||||
),
|
||||
WithListOptions(task.ListOptions{
|
||||
FormatTaskListAsJSON: true,
|
||||
}),
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
63
go.mod
63
go.mod
@@ -1,39 +1,66 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.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.4.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.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/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0
|
||||
github.com/go-task/template v0.1.0
|
||||
github.com/go-task/template v0.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-zglob v0.0.6
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/sajari/fuzzy v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/sebdah/goldie/v2 v2.7.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.17.0
|
||||
golang.org/x/term v0.35.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/v3 v3.9.0
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa
|
||||
mvdan.cc/sh/v3 v3.12.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.6 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // 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/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // 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/pierrec/lz4/v4 v4.1.22 // 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/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
201
go.sum
201
go.sum
@@ -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/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/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/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.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
||||
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.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.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
|
||||
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
|
||||
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
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/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
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=
|
||||
@@ -41,46 +94,102 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
|
||||
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 h1:ax+jBy7xFhh+Ka0IGLmH5mft+YDuqvzEjSgWuAP0nsM=
|
||||
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7/go.mod h1:/0Qr7qJeDwWxoKku2xKQ4Szc+SwBE3g9VE8jNiamsmc=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/moreinterp v0.0.0-20250807215248-5a1a658912aa h1:sRmA9AmA5+9CbK6a7N52q9W9jAeoBy1EJ7cncm+SLxw=
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
|
||||
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||
|
||||
86
help.go
86
help.go
@@ -24,15 +24,17 @@ type ListOptions struct {
|
||||
ListAllTasks bool
|
||||
FormatTaskListAsJSON bool
|
||||
NoStatus bool
|
||||
Nested bool
|
||||
}
|
||||
|
||||
// NewListOptions creates a new ListOptions instance
|
||||
func NewListOptions(list, listAll, listAsJson, noStatus bool) ListOptions {
|
||||
func NewListOptions(list, listAll, listAsJson, noStatus, nested bool) ListOptions {
|
||||
return ListOptions{
|
||||
ListOnlyTasksWithDescriptions: list,
|
||||
ListAllTasks: listAll,
|
||||
FormatTaskListAsJSON: listAsJson,
|
||||
NoStatus: noStatus,
|
||||
Nested: nested,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,20 +43,6 @@ func (o ListOptions) ShouldListTasks() bool {
|
||||
return o.ListOnlyTasksWithDescriptions || o.ListAllTasks
|
||||
}
|
||||
|
||||
// Validate validates that the collection of list-related options are in a valid configuration
|
||||
func (o ListOptions) Validate() error {
|
||||
if o.ListOnlyTasksWithDescriptions && o.ListAllTasks {
|
||||
return fmt.Errorf("task: cannot use --list and --list-all at the same time")
|
||||
}
|
||||
if o.FormatTaskListAsJSON && !o.ShouldListTasks() {
|
||||
return fmt.Errorf("task: --json only applies to --list or --list-all")
|
||||
}
|
||||
if o.NoStatus && !o.FormatTaskListAsJSON {
|
||||
return fmt.Errorf("task: --no-status only applies to --json with --list or --list-all")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filters returns the slice of FilterFunc which filters a list
|
||||
// of ast.Task according to the given ListOptions
|
||||
func (o ListOptions) Filters() []FilterFunc {
|
||||
@@ -77,7 +65,7 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
if o.FormatTaskListAsJSON {
|
||||
output, err := e.ToEditorOutput(tasks, o.NoStatus)
|
||||
output, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -128,18 +116,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 {
|
||||
@@ -153,32 +137,17 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Taskfile, error) {
|
||||
o := &editors.Taskfile{
|
||||
Tasks: make([]editors.Task, len(tasks)),
|
||||
Location: e.Taskfile.Location,
|
||||
}
|
||||
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) {
|
||||
var g errgroup.Group
|
||||
editorTasks := make([]editors.Task, len(tasks))
|
||||
|
||||
// Look over each task in parallel and turn it into an editor task
|
||||
for i := range tasks {
|
||||
aliases := []string{}
|
||||
if len(tasks[i].Aliases) > 0 {
|
||||
aliases = tasks[i].Aliases
|
||||
}
|
||||
g.Go(func() error {
|
||||
o.Tasks[i] = editors.Task{
|
||||
Name: tasks[i].Name(),
|
||||
Desc: tasks[i].Desc,
|
||||
Summary: tasks[i].Summary,
|
||||
Aliases: aliases,
|
||||
UpToDate: false,
|
||||
Location: &editors.Location{
|
||||
Line: tasks[i].Location.Line,
|
||||
Column: tasks[i].Location.Column,
|
||||
Taskfile: tasks[i].Location.Taskfile,
|
||||
},
|
||||
}
|
||||
editorTask := editors.NewTask(tasks[i])
|
||||
|
||||
if noStatus {
|
||||
editorTasks[i] = editorTask
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -197,10 +166,35 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
return err
|
||||
}
|
||||
|
||||
o.Tasks[i].UpToDate = upToDate
|
||||
|
||||
editorTask.UpToDate = &upToDate
|
||||
editorTasks[i] = editorTask
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return o, g.Wait()
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the root namespace
|
||||
var tasksLen int
|
||||
if !nested {
|
||||
tasksLen = len(editorTasks)
|
||||
}
|
||||
rootNamespace := &editors.Namespace{
|
||||
Tasks: make([]editors.Task, tasksLen),
|
||||
Location: e.Taskfile.Location,
|
||||
}
|
||||
|
||||
// Recursively add namespaces to the root namespace or if nesting is
|
||||
// disabled add them all to the root namespace
|
||||
for i, task := range editorTasks {
|
||||
taskNamespacePath := strings.Split(task.Task, ast.NamespaceSeparator)
|
||||
if nested {
|
||||
rootNamespace.AddNamespace(taskNamespacePath, task)
|
||||
} else {
|
||||
rootNamespace.Tasks[i] = task
|
||||
}
|
||||
}
|
||||
|
||||
return rootNamespace, g.Wait()
|
||||
}
|
||||
|
||||
52
init.go
52
init.go
@@ -1,41 +1,41 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
_ "embed"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
)
|
||||
|
||||
const defaultTaskfile = `# https://taskfile.dev
|
||||
const defaultTaskFilename = "Taskfile.yml"
|
||||
|
||||
version: '3'
|
||||
//go:embed taskfile/templates/default.yml
|
||||
var DefaultTaskfile string
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
silent: true
|
||||
`
|
||||
|
||||
const defaultTaskfileName = "Taskfile.yml"
|
||||
|
||||
// InitTaskfile Taskfile creates a new Taskfile
|
||||
func InitTaskfile(w io.Writer, dir string) error {
|
||||
f := filepathext.SmartJoin(dir, defaultTaskfileName)
|
||||
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return errors.TaskfileAlreadyExistsError{}
|
||||
// InitTaskfile creates a new Taskfile at path.
|
||||
//
|
||||
// path can be either a file path or a directory path.
|
||||
// If path is a directory, path/Taskfile.yml will be created.
|
||||
//
|
||||
// The final file path is always returned and may be different from the input path.
|
||||
func InitTaskfile(path string) (string, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err == nil && !fi.IsDir() {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(f, []byte(defaultTaskfile), 0o644); err != nil {
|
||||
return err
|
||||
if fi != nil && fi.IsDir() {
|
||||
path = filepathext.SmartJoin(path, defaultTaskFilename)
|
||||
// path was a directory, so check if Taskfile.yml exists in it
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "%s created in the current directory\n", defaultTaskfile)
|
||||
return nil
|
||||
|
||||
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
52
init_test.go
Normal file
52
init_test.go
Normal 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)
|
||||
}
|
||||
@@ -64,21 +64,15 @@ get_binaries() {
|
||||
case "$PLATFORM" in
|
||||
darwin/amd64) BINARIES="task" ;;
|
||||
darwin/arm64) BINARIES="task" ;;
|
||||
darwin/armv5) BINARIES="task" ;;
|
||||
darwin/armv6) BINARIES="task" ;;
|
||||
darwin/armv7) BINARIES="task" ;;
|
||||
darwin/arm) BINARIES="task" ;;
|
||||
linux/386) BINARIES="task" ;;
|
||||
linux/amd64) BINARIES="task" ;;
|
||||
linux/arm64) BINARIES="task" ;;
|
||||
linux/armv5) BINARIES="task" ;;
|
||||
linux/armv6) BINARIES="task" ;;
|
||||
linux/armv7) BINARIES="task" ;;
|
||||
linux/arm) BINARIES="task" ;;
|
||||
windows/386) BINARIES="task" ;;
|
||||
windows/amd64) BINARIES="task" ;;
|
||||
windows/arm64) BINARIES="task" ;;
|
||||
windows/armv5) BINARIES="task" ;;
|
||||
windows/armv6) BINARIES="task" ;;
|
||||
windows/armv7) BINARIES="task" ;;
|
||||
windows/arm) BINARIES="task" ;;
|
||||
*)
|
||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
||||
exit 1
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
package editors
|
||||
|
||||
import (
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type (
|
||||
// Taskfile wraps task list output for use in editor integrations (e.g. VSCode, etc)
|
||||
Taskfile struct {
|
||||
Tasks []Task `json:"tasks"`
|
||||
Location string `json:"location"`
|
||||
// Namespace wraps task list output for use in editor integrations (e.g. VSCode, etc)
|
||||
Namespace struct {
|
||||
Tasks []Task `json:"tasks"`
|
||||
Namespaces map[string]*Namespace `json:"namespaces,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
// Task describes a single task
|
||||
Task struct {
|
||||
Name string `json:"name"`
|
||||
Task string `json:"task"`
|
||||
Desc string `json:"desc"`
|
||||
Summary string `json:"summary"`
|
||||
Aliases []string `json:"aliases"`
|
||||
UpToDate bool `json:"up_to_date"`
|
||||
UpToDate *bool `json:"up_to_date,omitempty"`
|
||||
Location *Location `json:"location"`
|
||||
}
|
||||
// Location describes a task's location in a taskfile
|
||||
@@ -22,3 +28,59 @@ type (
|
||||
Taskfile string `json:"taskfile"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewTask(task *ast.Task) Task {
|
||||
aliases := []string{}
|
||||
if len(task.Aliases) > 0 {
|
||||
aliases = task.Aliases
|
||||
}
|
||||
return Task{
|
||||
Name: task.Name(),
|
||||
Task: task.Task,
|
||||
Desc: task.Desc,
|
||||
Summary: task.Summary,
|
||||
Aliases: aliases,
|
||||
Location: &Location{
|
||||
Line: task.Location.Line,
|
||||
Column: task.Location.Column,
|
||||
Taskfile: task.Location.Taskfile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
|
||||
if len(namespacePath) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// If there are no child namespaces, then we have found a task and we can
|
||||
// simply add it to the current namespace
|
||||
if len(namespacePath) == 1 {
|
||||
parent.Tasks = append(parent.Tasks, task)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the key of the current namespace in the path
|
||||
namespaceKey := namespacePath[0]
|
||||
|
||||
// Add the namespace to the parent namespaces map using the namespace key
|
||||
if parent.Namespaces == nil {
|
||||
parent.Namespaces = make(map[string]*Namespace, 0)
|
||||
}
|
||||
|
||||
// Search for the current namespace in the parent namespaces map
|
||||
// If it doesn't exist, create it
|
||||
namespace, ok := parent.Namespaces[namespaceKey]
|
||||
if !ok {
|
||||
namespace = &Namespace{}
|
||||
parent.Namespaces[namespaceKey] = namespace
|
||||
}
|
||||
|
||||
// Remove the current namespace key from the namespace path.
|
||||
childNamespacePath := namespacePath[1:]
|
||||
|
||||
// If there are no child namespaces in the task name, then we have found the
|
||||
// namespace of the task and we can add it to the current namespace.
|
||||
// Otherwise, we need to go deeper
|
||||
namespace.AddNamespace(childNamespacePath, task)
|
||||
}
|
||||
|
||||
31
internal/env/env.go
vendored
31
internal/env/env.go
vendored
@@ -3,21 +3,42 @@ package env
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
const taskVarPrefix = "TASK_"
|
||||
|
||||
// GetEnviron the all return all environment variables encapsulated on a
|
||||
// ast.Vars
|
||||
func GetEnviron() *ast.Vars {
|
||||
m := ast.NewVars()
|
||||
for _, e := range os.Environ() {
|
||||
keyVal := strings.SplitN(e, "=", 2)
|
||||
key, val := keyVal[0], keyVal[1]
|
||||
m.Set(key, ast.Var{Value: val})
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func Get(t *ast.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return GetFromVars(t.Env)
|
||||
}
|
||||
|
||||
func GetFromVars(env *ast.Vars) []string {
|
||||
environ := os.Environ()
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
|
||||
for k, v := range env.ToCacheMap() {
|
||||
if !isTypeAllowed(v) {
|
||||
continue
|
||||
}
|
||||
if !experiments.EnvPrecedence.Enabled {
|
||||
if !experiments.EnvPrecedence.Enabled() {
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
@@ -36,3 +57,7 @@ func isTypeAllowed(v any) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func GetTaskEnv(key string) string {
|
||||
return os.Getenv(taskVarPrefix + key)
|
||||
}
|
||||
|
||||
20
internal/execext/coreutils.go
Normal file
20
internal/execext/coreutils.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package execext
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
)
|
||||
|
||||
var useGoCoreUtils bool
|
||||
|
||||
func init() {
|
||||
// If TASK_CORE_UTILS is set to either true or false, respect that.
|
||||
// By default, enable on Windows only.
|
||||
if v, err := strconv.ParseBool(env.GetTaskEnv("CORE_UTILS")); err == nil {
|
||||
useGoCoreUtils = v
|
||||
} else {
|
||||
useGoCoreUtils = runtime.GOOS == "windows"
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,24 @@ package execext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mvdan.cc/sh/moreinterp/coreutils"
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// RunCommandOptions is the options for the RunCommand func
|
||||
// ErrNilOptions is returned when a nil options is given
|
||||
var ErrNilOptions = errors.New("execext: nil options given")
|
||||
|
||||
// RunCommandOptions is the options for the [RunCommand] func.
|
||||
type RunCommandOptions struct {
|
||||
Command string
|
||||
Dir string
|
||||
@@ -28,9 +31,6 @@ type RunCommandOptions struct {
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
// ErrNilOptions is returned when a nil options is given
|
||||
var ErrNilOptions = errors.New("execext: nil options given")
|
||||
|
||||
// RunCommand runs a shell command
|
||||
func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
if opts == nil {
|
||||
@@ -59,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
r, err := interp.New(
|
||||
interp.Params(params...),
|
||||
interp.Env(expand.ListEnviron(environ...)),
|
||||
interp.ExecHandlers(execHandler),
|
||||
interp.ExecHandlers(execHandlers()...),
|
||||
interp.OpenHandler(openHandler),
|
||||
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
|
||||
dirOption(opts.Dir),
|
||||
@@ -90,26 +90,64 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
return r.Run(ctx, p)
|
||||
}
|
||||
|
||||
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
|
||||
// if available.
|
||||
func Expand(s string) (string, error) {
|
||||
func escape(s string) string {
|
||||
s = filepath.ToSlash(s)
|
||||
s = strings.ReplaceAll(s, " ", `\ `)
|
||||
s = strings.ReplaceAll(s, "&", `\&`)
|
||||
s = strings.ReplaceAll(s, "(", `\(`)
|
||||
s = strings.ReplaceAll(s, ")", `\)`)
|
||||
fields, err := shell.Fields(s, nil)
|
||||
return s
|
||||
}
|
||||
|
||||
// ExpandLiteral is a wrapper around [expand.Literal]. It will escape the input
|
||||
// string, expand any shell symbols (such as '~') and resolve any environment
|
||||
// variables.
|
||||
func ExpandLiteral(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", nil
|
||||
}
|
||||
p := syntax.NewParser()
|
||||
word, err := p.Document(strings.NewReader(s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
return fields[0], nil
|
||||
cfg := &expand.Config{
|
||||
Env: expand.FuncEnviron(os.Getenv),
|
||||
ReadDir2: os.ReadDir,
|
||||
GlobStar: true,
|
||||
}
|
||||
return "", nil
|
||||
return expand.Literal(cfg, word)
|
||||
}
|
||||
|
||||
func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
|
||||
return interp.DefaultExecHandler(15 * time.Second)
|
||||
// ExpandFields is a wrapper around [expand.Fields]. It will escape the input
|
||||
// string, expand any shell symbols (such as '~') and resolve any environment
|
||||
// variables. It also expands brace expressions ({a.b}) and globs (*/**) and
|
||||
// returns the results as a list of strings.
|
||||
func ExpandFields(s string) ([]string, error) {
|
||||
s = escape(s)
|
||||
p := syntax.NewParser()
|
||||
var words []*syntax.Word
|
||||
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
|
||||
words = append(words, w)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &expand.Config{
|
||||
Env: expand.FuncEnviron(os.Getenv),
|
||||
ReadDir2: os.ReadDir,
|
||||
GlobStar: true,
|
||||
NullGlob: true,
|
||||
}
|
||||
return expand.Fields(cfg, words...)
|
||||
}
|
||||
|
||||
func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) {
|
||||
if useGoCoreUtils {
|
||||
handlers = append(handlers, coreutils.ExecHandler)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// This package is intended as a place to copy functions from the
|
||||
// golang.org/x/exp package. Copying these functions allows us to rely on our
|
||||
// own code instead of an external package that may change unpredictably in the
|
||||
// future.
|
||||
//
|
||||
// It also prevents problems with transitive dependencies whereby a
|
||||
// package that imports Task (and therefore our version of golang.org/x/exp)
|
||||
// cannot import a different version of golang.org/x/exp.
|
||||
//
|
||||
// Finally, it serves as a place to track functions that may be able to be
|
||||
// removed in the future if they are added to the standard library. This is also
|
||||
// why this package is under the internal directory since these functions are
|
||||
// not intended to be used outside of Task.
|
||||
package exp
|
||||
|
||||
import "cmp"
|
||||
|
||||
// Keys is a copy of https://pkg.go.dev/golang.org/x/exp@v0.0.0-20240103183307-be819d1f06fc/maps#Keys.
|
||||
// This is not yet included in the standard library. See https://github.com/golang/go/issues/61538.
|
||||
func Keys[K cmp.Ordered, V any](m map[K]V) []K {
|
||||
var keys []K
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
)
|
||||
|
||||
const envPrefix = "TASK_X_"
|
||||
|
||||
type Experiment struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Value string
|
||||
}
|
||||
|
||||
// A list of experiments.
|
||||
var (
|
||||
GentleForce Experiment
|
||||
RemoteTaskfiles Experiment
|
||||
AnyVariables Experiment
|
||||
MapVariables Experiment
|
||||
EnvPrecedence Experiment
|
||||
)
|
||||
|
||||
func init() {
|
||||
readDotEnv()
|
||||
GentleForce = New("GENTLE_FORCE")
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES")
|
||||
AnyVariables = New("ANY_VARIABLES", "1", "2")
|
||||
MapVariables = New("MAP_VARIABLES", "1", "2")
|
||||
EnvPrecedence = New("ENV_PRECEDENCE")
|
||||
}
|
||||
|
||||
func New(xName string, enabledValues ...string) Experiment {
|
||||
if len(enabledValues) == 0 {
|
||||
enabledValues = []string{"1"}
|
||||
}
|
||||
value := getEnv(xName)
|
||||
return Experiment{
|
||||
Name: xName,
|
||||
Enabled: slices.Contains(enabledValues, value),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (x Experiment) String() string {
|
||||
if x.Enabled {
|
||||
return fmt.Sprintf("on (%s)", x.Value)
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
func getEnv(xName string) string {
|
||||
envName := fmt.Sprintf("%s%s", envPrefix, xName)
|
||||
return os.Getenv(envName)
|
||||
}
|
||||
|
||||
func getEnvFilePath() string {
|
||||
// Parse the CLI flags again to get the directory/taskfile being run
|
||||
// We use a flagset here so that we can parse a subset of flags without exiting on error.
|
||||
var dir, taskfile string
|
||||
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
||||
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
||||
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
// If the directory is set, find a .env file in that directory.
|
||||
if dir != "" {
|
||||
return filepath.Join(dir, ".env")
|
||||
}
|
||||
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
|
||||
if taskfile != "" {
|
||||
return filepath.Join(filepath.Dir(taskfile), ".env")
|
||||
}
|
||||
// Otherwise just use the current working directory.
|
||||
return ".env"
|
||||
}
|
||||
|
||||
func readDotEnv() {
|
||||
env, _ := godotenv.Read(getEnvFilePath())
|
||||
// If the env var is an experiment, set it.
|
||||
for key, value := range env {
|
||||
if strings.HasPrefix(key, envPrefix) {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printExperiment(w io.Writer, l *logger.Logger, x Experiment) {
|
||||
l.FOutf(w, logger.Yellow, "* ")
|
||||
l.FOutf(w, logger.Green, x.Name)
|
||||
l.FOutf(w, logger.Default, ": \t%s\n", x.String())
|
||||
}
|
||||
|
||||
func List(l *logger.Logger) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0)
|
||||
printExperiment(w, l, GentleForce)
|
||||
printExperiment(w, l, RemoteTaskfiles)
|
||||
printExperiment(w, l, MapVariables)
|
||||
printExperiment(w, l, EnvPrecedence)
|
||||
return w.Flush()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
320
internal/fingerprint/checker_mock.go
Normal file
320
internal/fingerprint/checker_mock.go
Normal file
@@ -0,0 +1,320 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewMockStatusCheckable creates a new instance of MockStatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockStatusCheckable(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockStatusCheckable {
|
||||
mock := &MockStatusCheckable{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockStatusCheckable is an autogenerated mock type for the StatusCheckable type
|
||||
type MockStatusCheckable struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockStatusCheckable_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockStatusCheckable) EXPECT() *MockStatusCheckable_Expecter {
|
||||
return &MockStatusCheckable_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// IsUpToDate provides a mock function for the type MockStatusCheckable
|
||||
func (_mock *MockStatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {
|
||||
ret := _mock.Called(ctx, t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsUpToDate")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok {
|
||||
return returnFunc(ctx, t)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok {
|
||||
r0 = returnFunc(ctx, t)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok {
|
||||
r1 = returnFunc(ctx, t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockStatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
|
||||
type MockStatusCheckable_IsUpToDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsUpToDate is a helper method to define mock.On call
|
||||
// - ctx
|
||||
// - t
|
||||
func (_e *MockStatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *MockStatusCheckable_IsUpToDate_Call {
|
||||
return &MockStatusCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", ctx, t)}
|
||||
}
|
||||
|
||||
func (_c *MockStatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *MockStatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStatusCheckable_IsUpToDate_Call) Return(b bool, err error) *MockStatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(b, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStatusCheckable_IsUpToDate_Call) RunAndReturn(run func(ctx context.Context, t *ast.Task) (bool, error)) *MockStatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSourcesCheckable creates a new instance of MockSourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockSourcesCheckable(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSourcesCheckable {
|
||||
mock := &MockSourcesCheckable{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockSourcesCheckable is an autogenerated mock type for the SourcesCheckable type
|
||||
type MockSourcesCheckable struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSourcesCheckable_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSourcesCheckable) EXPECT() *MockSourcesCheckable_Expecter {
|
||||
return &MockSourcesCheckable_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// IsUpToDate provides a mock function for the type MockSourcesCheckable
|
||||
func (_mock *MockSourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
ret := _mock.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsUpToDate")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok {
|
||||
return returnFunc(t)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(*ast.Task) bool); ok {
|
||||
r0 = returnFunc(t)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {
|
||||
r1 = returnFunc(t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
|
||||
type MockSourcesCheckable_IsUpToDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsUpToDate is a helper method to define mock.On call
|
||||
// - t
|
||||
func (_e *MockSourcesCheckable_Expecter) IsUpToDate(t interface{}) *MockSourcesCheckable_IsUpToDate_Call {
|
||||
return &MockSourcesCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", t)}
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_IsUpToDate_Call) Return(b bool, err error) *MockSourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(b, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(t *ast.Task) (bool, error)) *MockSourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Kind provides a mock function for the type MockSourcesCheckable
|
||||
func (_mock *MockSourcesCheckable) Kind() string {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Kind")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if returnFunc, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockSourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind'
|
||||
type MockSourcesCheckable_Kind_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Kind is a helper method to define mock.On call
|
||||
func (_e *MockSourcesCheckable_Expecter) Kind() *MockSourcesCheckable_Kind_Call {
|
||||
return &MockSourcesCheckable_Kind_Call{Call: _e.mock.On("Kind")}
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Kind_Call) Run(run func()) *MockSourcesCheckable_Kind_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Kind_Call) Return(s string) *MockSourcesCheckable_Kind_Call {
|
||||
_c.Call.Return(s)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Kind_Call) RunAndReturn(run func() string) *MockSourcesCheckable_Kind_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnError provides a mock function for the type MockSourcesCheckable
|
||||
func (_mock *MockSourcesCheckable) OnError(t *ast.Task) error {
|
||||
ret := _mock.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnError")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*ast.Task) error); ok {
|
||||
r0 = returnFunc(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockSourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError'
|
||||
type MockSourcesCheckable_OnError_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnError is a helper method to define mock.On call
|
||||
// - t
|
||||
func (_e *MockSourcesCheckable_Expecter) OnError(t interface{}) *MockSourcesCheckable_OnError_Call {
|
||||
return &MockSourcesCheckable_OnError_Call{Call: _e.mock.On("OnError", t)}
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_OnError_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_OnError_Call) Return(err error) *MockSourcesCheckable_OnError_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_OnError_Call) RunAndReturn(run func(t *ast.Task) error) *MockSourcesCheckable_OnError_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Value provides a mock function for the type MockSourcesCheckable
|
||||
func (_mock *MockSourcesCheckable) Value(t *ast.Task) (any, error) {
|
||||
ret := _mock.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Value")
|
||||
}
|
||||
|
||||
var r0 any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*ast.Task) (any, error)); ok {
|
||||
return returnFunc(t)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(*ast.Task) any); ok {
|
||||
r0 = returnFunc(t)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {
|
||||
r1 = returnFunc(t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'
|
||||
type MockSourcesCheckable_Value_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Value is a helper method to define mock.On call
|
||||
// - t
|
||||
func (_e *MockSourcesCheckable_Expecter) Value(t interface{}) *MockSourcesCheckable_Value_Call {
|
||||
return &MockSourcesCheckable_Value_Call{Call: _e.mock.On("Value", t)}
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_Value_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Value_Call) Return(v any, err error) *MockSourcesCheckable_Value_Call {
|
||||
_c.Call.Return(v, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSourcesCheckable_Value_Call) RunAndReturn(run func(t *ast.Task) (any, error)) *MockSourcesCheckable_Value_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -4,47 +4,34 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/mattn/go-zglob"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func Globs(dir string, globs []*ast.Glob) ([]string, error) {
|
||||
fileMap := make(map[string]bool)
|
||||
resultMap := make(map[string]bool)
|
||||
for _, g := range globs {
|
||||
matches, err := Glob(dir, g.Glob)
|
||||
matches, err := glob(dir, g.Glob)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, match := range matches {
|
||||
fileMap[match] = !g.Negate
|
||||
resultMap[match] = !g.Negate
|
||||
}
|
||||
}
|
||||
files := make([]string, 0)
|
||||
for file, includePath := range fileMap {
|
||||
if includePath {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
return files, nil
|
||||
return collectKeys(resultMap), nil
|
||||
}
|
||||
|
||||
func Glob(dir string, g string) ([]string, error) {
|
||||
files := make([]string, 0)
|
||||
func glob(dir string, g string) ([]string, error) {
|
||||
g = filepathext.SmartJoin(dir, g)
|
||||
|
||||
g, err := execext.Expand(g)
|
||||
fs, err := execext.ExpandFields(g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs, err := zglob.GlobFollowSymlinks(g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := make(map[string]bool, len(fs))
|
||||
|
||||
for _, f := range fs {
|
||||
info, err := os.Stat(f)
|
||||
@@ -54,7 +41,18 @@ func Glob(dir string, g string) ([]string, error) {
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
files = append(files, f)
|
||||
results[f] = true
|
||||
}
|
||||
return files, nil
|
||||
return collectKeys(results), nil
|
||||
}
|
||||
|
||||
func collectKeys(m map[string]bool) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
if v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
if g.Negate {
|
||||
continue
|
||||
}
|
||||
generates, err := Glob(t.Dir, g.Glob)
|
||||
generates, err := glob(t.Dir, g.Glob)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func TestNormalizeFilename(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
In, Out string
|
||||
}{
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/internal/mocks"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -26,11 +24,13 @@ import (
|
||||
// | false | true | false |
|
||||
// | false | false | false |
|
||||
func TestIsTaskUpToDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *ast.Task
|
||||
setupMockStatusChecker func(m *mocks.StatusCheckable)
|
||||
setupMockSourcesChecker func(m *mocks.SourcesCheckable)
|
||||
setupMockStatusChecker func(m *MockStatusCheckable)
|
||||
setupMockSourcesChecker func(m *MockSourcesCheckable)
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
@@ -50,7 +50,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
@@ -62,7 +62,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
@@ -73,7 +73,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
@@ -85,10 +85,10 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
@@ -99,10 +99,10 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
@@ -113,7 +113,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
@@ -125,10 +125,10 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
||||
},
|
||||
expected: false,
|
||||
@@ -139,10 +139,10 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
Status: []string{"status"},
|
||||
Sources: []*ast.Glob{{Glob: "sources"}},
|
||||
},
|
||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
@@ -150,18 +150,20 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockStatusChecker := mocks.NewStatusCheckable(t)
|
||||
t.Parallel()
|
||||
|
||||
mockStatusChecker := NewMockStatusCheckable(t)
|
||||
if tt.setupMockStatusChecker != nil {
|
||||
tt.setupMockStatusChecker(mockStatusChecker)
|
||||
}
|
||||
|
||||
mockSourcesChecker := mocks.NewSourcesCheckable(t)
|
||||
mockSourcesChecker := NewMockSourcesCheckable(t)
|
||||
if tt.setupMockSourcesChecker != nil {
|
||||
tt.setupMockSourcesChecker(mockSourcesChecker)
|
||||
}
|
||||
|
||||
result, err := IsTaskUpToDate(
|
||||
context.Background(),
|
||||
t.Context(),
|
||||
tt.task,
|
||||
WithStatusChecker(mockStatusChecker),
|
||||
WithSourcesChecker(mockSourcesChecker),
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"cmp"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
"github.com/go-task/task/v3/taskrc"
|
||||
taskrcast "github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
|
||||
const usage = `Usage: task [flags...] [task...]
|
||||
@@ -35,42 +41,66 @@ Options:
|
||||
`
|
||||
|
||||
var (
|
||||
Version bool
|
||||
Help bool
|
||||
Init bool
|
||||
Completion string
|
||||
List bool
|
||||
ListAll bool
|
||||
ListJson bool
|
||||
TaskSort string
|
||||
Status bool
|
||||
NoStatus bool
|
||||
Insecure bool
|
||||
Force bool
|
||||
ForceAll bool
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
AssumeYes bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
ExitCode bool
|
||||
Parallel bool
|
||||
Concurrency int
|
||||
Dir string
|
||||
Entrypoint string
|
||||
Output ast.Output
|
||||
Color bool
|
||||
Interval time.Duration
|
||||
Global bool
|
||||
Experiments bool
|
||||
Download bool
|
||||
Offline bool
|
||||
ClearCache bool
|
||||
Timeout time.Duration
|
||||
Version bool
|
||||
Help bool
|
||||
Init bool
|
||||
Completion string
|
||||
List bool
|
||||
ListAll bool
|
||||
ListJson bool
|
||||
TaskSort string
|
||||
Status bool
|
||||
NoStatus bool
|
||||
Nested bool
|
||||
Insecure bool
|
||||
Force bool
|
||||
ForceAll bool
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
AssumeYes bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
ExitCode bool
|
||||
Parallel bool
|
||||
Concurrency int
|
||||
Dir string
|
||||
Entrypoint string
|
||||
Output ast.Output
|
||||
Color bool
|
||||
Interval time.Duration
|
||||
Global bool
|
||||
Experiments bool
|
||||
Download bool
|
||||
Offline bool
|
||||
ClearCache bool
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Config files can enable experiments which alter the availability and/or
|
||||
// behavior of some flags, so we need to parse the experiments before the
|
||||
// flags. However, we need the --taskfile and --dir flags before we can
|
||||
// parse the experiments as they can alter the location of the config files.
|
||||
// Because of this circular dependency, we parse the flags twice. First, we
|
||||
// get the --taskfile and --dir flags, then we parse the experiments, then
|
||||
// we parse the flags again to get the full set. We use a flagset here so
|
||||
// that we can parse a subset of flags without exiting on error.
|
||||
var dir, entrypoint string
|
||||
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
||||
fs.StringVarP(&dir, "dir", "d", "", "")
|
||||
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
|
||||
fs.Usage = func() {}
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
|
||||
// Parse the experiments
|
||||
dir = cmp.Or(dir, filepath.Dir(entrypoint))
|
||||
|
||||
config, _ := taskrc.GetConfig(dir)
|
||||
experiments.ParseWithConfig(dir, config)
|
||||
|
||||
// Parse the rest of the flags
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stderr)
|
||||
pflag.Usage = func() {
|
||||
@@ -88,29 +118,30 @@ func init() {
|
||||
pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].")
|
||||
pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
|
||||
pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON")
|
||||
pflag.BoolVar(&Insecure, "insecure", false, "Forces Task to download Taskfiles over insecure connections.")
|
||||
pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON")
|
||||
pflag.BoolVar(&Insecure, "insecure", getConfig(config, func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
|
||||
pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.")
|
||||
pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.")
|
||||
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.")
|
||||
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
|
||||
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
|
||||
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
|
||||
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
|
||||
pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
|
||||
pflag.StringVarP(&Dir, "dir", "d", "", "Sets directory of execution.")
|
||||
pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.")
|
||||
pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
|
||||
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
|
||||
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", getConfig(config, func() *int { return config.Concurrency }, 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,13 +149,13 @@ 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.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
|
||||
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
|
||||
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
|
||||
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
|
||||
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, 0), "Expiry duration for cached remote Taskfiles.")
|
||||
}
|
||||
|
||||
pflag.Parse()
|
||||
}
|
||||
|
||||
@@ -138,8 +169,7 @@ func Validate() error {
|
||||
}
|
||||
|
||||
if Global && Dir != "" {
|
||||
log.Fatal("task: You can't set both --global and --dir")
|
||||
return nil
|
||||
return errors.New("task: You can't set both --global and --dir")
|
||||
}
|
||||
|
||||
if Output.Name != "group" {
|
||||
@@ -154,5 +184,87 @@ func Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if List && ListAll {
|
||||
return errors.New("task: cannot use --list and --list-all at the same time")
|
||||
}
|
||||
|
||||
if ListJson && !List && !ListAll {
|
||||
return errors.New("task: --json only applies to --list or --list-all")
|
||||
}
|
||||
|
||||
if NoStatus && !ListJson {
|
||||
return errors.New("task: --no-status only applies to --json with --list or --list-all")
|
||||
}
|
||||
|
||||
if Nested && !ListJson {
|
||||
return errors.New("task: --nested only applies to --json with --list or --list-all")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithFlags is a special internal functional option that is used to pass flags
|
||||
// from the CLI into any constructor that accepts functional options.
|
||||
func WithFlags() task.ExecutorOption {
|
||||
return &flagsOption{}
|
||||
}
|
||||
|
||||
type flagsOption struct{}
|
||||
|
||||
func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
// Set the sorter
|
||||
var sorter sort.Sorter
|
||||
switch TaskSort {
|
||||
case "none":
|
||||
sorter = sort.NoSort
|
||||
case "alphanumeric":
|
||||
sorter = sort.AlphaNumeric
|
||||
}
|
||||
|
||||
// Change the directory to the user's home directory if the global flag is set
|
||||
dir := Dir
|
||||
if Global {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
dir = home
|
||||
}
|
||||
}
|
||||
|
||||
e.Options(
|
||||
task.WithDir(dir),
|
||||
task.WithEntrypoint(Entrypoint),
|
||||
task.WithForce(Force),
|
||||
task.WithForceAll(ForceAll),
|
||||
task.WithInsecure(Insecure),
|
||||
task.WithDownload(Download),
|
||||
task.WithOffline(Offline),
|
||||
task.WithTimeout(Timeout),
|
||||
task.WithCacheExpiryDuration(CacheExpiryDuration),
|
||||
task.WithWatch(Watch),
|
||||
task.WithVerbose(Verbose),
|
||||
task.WithSilent(Silent),
|
||||
task.WithAssumeYes(AssumeYes),
|
||||
task.WithDry(Dry || Status),
|
||||
task.WithSummary(Summary),
|
||||
task.WithParallel(Parallel),
|
||||
task.WithColor(Color),
|
||||
task.WithConcurrency(Concurrency),
|
||||
task.WithInterval(Interval),
|
||||
task.WithOutputStyle(Output),
|
||||
task.WithTaskSorter(sorter),
|
||||
task.WithVersionCheck(true),
|
||||
)
|
||||
}
|
||||
|
||||
// getConfig extracts a config value directly from a pointer field with a fallback default
|
||||
func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T {
|
||||
if config == nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
field := fieldFunc()
|
||||
if field != nil {
|
||||
return *field
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
202
internal/fsext/fs.go
Normal file
202
internal/fsext/fs.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package fsext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/sysinfo"
|
||||
)
|
||||
|
||||
// DefaultDir will return the default directory given an entrypoint or
|
||||
// directory. If the directory is set, it will ensure it is an absolute path and
|
||||
// return it. If the entrypoint is set, but the directory is not, it will leave
|
||||
// the directory blank. If both are empty, it will default the directory to the
|
||||
// current working directory.
|
||||
func DefaultDir(entrypoint, dir string) string {
|
||||
// If the directory is set, ensure it is an absolute path
|
||||
if dir != "" {
|
||||
var err error
|
||||
dir, err = filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// If the entrypoint and dir are empty, we default the directory to the current working directory
|
||||
if entrypoint == "" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return wd
|
||||
}
|
||||
|
||||
// If the entrypoint is set, but the directory is not, we leave the directory blank
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveDir returns an absolute path to the directory that the task should be
|
||||
// run in. If the entrypoint and dir are BOTH set, then the Taskfile will not
|
||||
// sit inside the directory specified by dir and we should ensure that the dir
|
||||
// is absolute. Otherwise, the dir will always be the parent directory of the
|
||||
// resolved entrypoint, so we should return that parent directory.
|
||||
func ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) {
|
||||
if entrypoint != "" && dir != "" {
|
||||
return filepath.Abs(dir)
|
||||
}
|
||||
return filepath.Dir(resolvedEntrypoint), nil
|
||||
}
|
||||
|
||||
// Search looks for files with the given possible filenames using the given
|
||||
// entrypoint and directory. If the entrypoint is set, it checks if the
|
||||
// entrypoint matches a file or if it matches a directory containing one of the
|
||||
// possible filenames. Otherwise, it walks up the file tree starting at the
|
||||
// given directory and performs a search in each directory for the possible
|
||||
// filenames until it finds a match or reaches the root directory. If the
|
||||
// entrypoint and directory are both empty, it defaults the directory to the
|
||||
// current working directory and performs a recursive search starting there. If
|
||||
// a match is found, the absolute path to the file is returned with its
|
||||
// directory. If no match is found, an error is returned.
|
||||
func Search(entrypoint, dir string, possibleFilenames []string) (string, error) {
|
||||
var err error
|
||||
if entrypoint != "" {
|
||||
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return entrypoint, nil
|
||||
}
|
||||
if dir == "" {
|
||||
dir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
// SearchAll looks for files with the given possible filenames using the given
|
||||
// entrypoint and directory. If the entrypoint is set, it checks if the
|
||||
// entrypoint matches a file or if it matches a directory containing one of the
|
||||
// possible filenames and add it to a list of matches. It then walks up the file
|
||||
// tree starting at the given directory and performs a search in each directory
|
||||
// for the possible filenames until it finds a match or reaches the root
|
||||
// directory. If the entrypoint and directory are both empty, it defaults the
|
||||
// directory to the current working directory and performs a recursive search
|
||||
// starting there. If matches are found, the absolute path to each file is added
|
||||
// to the list and returned.
|
||||
func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) {
|
||||
var err error
|
||||
var entrypoints []string
|
||||
if entrypoint != "" {
|
||||
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entrypoints = append(entrypoints, entrypoint)
|
||||
}
|
||||
if dir == "" {
|
||||
dir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
paths, err := SearchNPathRecursively(dir, possibleFilenames, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(entrypoints, paths...), nil
|
||||
}
|
||||
|
||||
// SearchPath will check if a file at the given path exists or not. If it does,
|
||||
// it will return the path to it. If it does not, it will search for any files
|
||||
// at the given path with any of the given possible names. If any of these match
|
||||
// a file, the first matching path will be returned. If no files are found, an
|
||||
// error will be returned.
|
||||
func SearchPath(path string, possibleFilenames []string) (string, error) {
|
||||
// Get file info about the path
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the path exists and is a regular file, device, symlink, or named pipe,
|
||||
// return the absolute path to it
|
||||
if fi.Mode().IsRegular() ||
|
||||
fi.Mode()&os.ModeDevice != 0 ||
|
||||
fi.Mode()&os.ModeSymlink != 0 ||
|
||||
fi.Mode()&os.ModeNamedPipe != 0 {
|
||||
return filepath.Abs(path)
|
||||
}
|
||||
|
||||
// If the path is a directory, check if any of the possible names exist
|
||||
// in that directory
|
||||
for _, filename := range possibleFilenames {
|
||||
alt := filepathext.SmartJoin(path, filename)
|
||||
if _, err := os.Stat(alt); err == nil {
|
||||
return filepath.Abs(alt)
|
||||
}
|
||||
}
|
||||
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
// SearchPathRecursively walks up the directory tree starting at the given
|
||||
// path, calling the Search function in each directory until it finds a matching
|
||||
// file or reaches the root directory. On supported operating systems, it will
|
||||
// also check if the user ID of the directory changes and abort if it does.
|
||||
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
|
||||
paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return paths[0], nil
|
||||
}
|
||||
|
||||
// SearchNPathRecursively walks up the directory tree starting at the given
|
||||
// path, calling the Search function in each directory and adding each matching
|
||||
// file that it finds to a list until it reaches the root directory or the
|
||||
// length of the list exceeds n. On supported operating systems, it will also
|
||||
// check if the user ID of the directory changes and abort if it does.
|
||||
func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
owner, err := sysinfo.Owner(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for n == -1 || len(paths) < n {
|
||||
fpath, err := SearchPath(path, possibleFilenames)
|
||||
if err == nil {
|
||||
paths = append(paths, fpath)
|
||||
}
|
||||
|
||||
// Get the parent path/user id
|
||||
parentPath := filepath.Dir(path)
|
||||
parentOwner, err := sysinfo.Owner(parentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Error if we reached the root directory and still haven't found a file
|
||||
// OR if the user id of the directory changes
|
||||
if path == parentPath || (parentOwner != owner) {
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
owner = parentOwner
|
||||
path = parentPath
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
224
internal/fsext/fs_test.go
Normal file
224
internal/fsext/fs_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package fsext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entrypoint string
|
||||
dir string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "default to current working directory",
|
||||
entrypoint: "",
|
||||
dir: "",
|
||||
expected: wd,
|
||||
},
|
||||
{
|
||||
name: "resolves relative dir path",
|
||||
entrypoint: "",
|
||||
dir: "./dir",
|
||||
expected: filepath.Join(wd, "dir"),
|
||||
},
|
||||
{
|
||||
name: "return entrypoint if set",
|
||||
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||
dir: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "if entrypoint and dir are set",
|
||||
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||
dir: filepath.Join(wd, "dir"),
|
||||
expected: filepath.Join(wd, "dir"),
|
||||
},
|
||||
{
|
||||
name: "if entrypoint and dir are set and dir is relative",
|
||||
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||
dir: "./dir",
|
||||
expected: filepath.Join(wd, "dir"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tt.expected, DefaultDir(tt.entrypoint, tt.dir))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entrypoint string
|
||||
dir string
|
||||
possibleFilenames []string
|
||||
expectedEntrypoint string
|
||||
}{
|
||||
{
|
||||
name: "find foo.txt using relative entrypoint",
|
||||
entrypoint: "./testdata/foo.txt",
|
||||
possibleFilenames: []string{"foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using absolute entrypoint",
|
||||
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
possibleFilenames: []string{"foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using relative dir",
|
||||
dir: "./testdata",
|
||||
possibleFilenames: []string{"foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using absolute dir",
|
||||
dir: filepath.Join(wd, "testdata"),
|
||||
possibleFilenames: []string{"foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using relative dir and relative entrypoint",
|
||||
entrypoint: "./testdata/foo.txt",
|
||||
dir: "./testdata/some/other/dir",
|
||||
possibleFilenames: []string{"foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find fs.go using no entrypoint or dir",
|
||||
entrypoint: "",
|
||||
dir: "",
|
||||
possibleFilenames: []string{"fs.go"},
|
||||
expectedEntrypoint: filepath.Join(wd, "fs.go"),
|
||||
},
|
||||
{
|
||||
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
|
||||
entrypoint: "",
|
||||
dir: "",
|
||||
possibleFilenames: []string{"Taskfile.yml"},
|
||||
expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt first if listed first in possible filenames",
|
||||
entrypoint: "./testdata",
|
||||
possibleFilenames: []string{"foo.txt", "bar.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
},
|
||||
{
|
||||
name: "find bar.txt first if listed first in possible filenames",
|
||||
entrypoint: "./testdata",
|
||||
possibleFilenames: []string{"bar.txt", "foo.txt"},
|
||||
expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
entrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedEntrypoint, entrypoint)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entrypoint string
|
||||
resolvedEntrypoint string
|
||||
dir string
|
||||
expectedDir string
|
||||
}{
|
||||
{
|
||||
name: "find foo.txt using relative entrypoint",
|
||||
entrypoint: "./testdata/foo.txt",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using absolute entrypoint",
|
||||
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using relative dir",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
dir: "./testdata",
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using absolute dir",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
dir: filepath.Join(wd, "testdata"),
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt using relative dir and relative entrypoint",
|
||||
entrypoint: "./testdata/foo.txt",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
dir: "./testdata/some/other/dir",
|
||||
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
|
||||
},
|
||||
{
|
||||
name: "find fs.go using no entrypoint or dir",
|
||||
entrypoint: "",
|
||||
resolvedEntrypoint: filepath.Join(wd, "fs.go"),
|
||||
dir: "",
|
||||
expectedDir: wd,
|
||||
},
|
||||
{
|
||||
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
|
||||
entrypoint: "",
|
||||
resolvedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
|
||||
dir: "",
|
||||
expectedDir: filepath.Join(wd, "..", ".."),
|
||||
},
|
||||
{
|
||||
name: "find foo.txt first if listed first in possible filenames",
|
||||
entrypoint: "./testdata",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
{
|
||||
name: "find bar.txt first if listed first in possible filenames",
|
||||
entrypoint: "./testdata",
|
||||
resolvedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
|
||||
expectedDir: filepath.Join(wd, "testdata"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedDir, dir)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
51
internal/fsnotifyext/fsnotify_dedup.go
Normal file
51
internal/fsnotifyext/fsnotify_dedup.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package fsnotifyext
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
type Deduper struct {
|
||||
w *fsnotify.Watcher
|
||||
waitTime time.Duration
|
||||
}
|
||||
|
||||
func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
|
||||
return &Deduper{
|
||||
w: w,
|
||||
waitTime: waitTime,
|
||||
}
|
||||
}
|
||||
|
||||
// GetChan returns a chan of deduplicated [fsnotify.Event].
|
||||
//
|
||||
// [fsnotify.Chmod] operations will be skipped.
|
||||
func (d *Deduper) GetChan() <-chan fsnotify.Event {
|
||||
channel := make(chan fsnotify.Event)
|
||||
|
||||
go func() {
|
||||
timers := make(map[string]*time.Timer)
|
||||
for {
|
||||
event, ok := <-d.w.Events
|
||||
switch {
|
||||
case !ok:
|
||||
return
|
||||
case event.Has(fsnotify.Chmod):
|
||||
continue
|
||||
}
|
||||
|
||||
timer, ok := timers[event.String()]
|
||||
if !ok {
|
||||
timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
|
||||
timer.Stop()
|
||||
timers[event.String()] = timer
|
||||
}
|
||||
|
||||
timer.Reset(d.waitTime)
|
||||
}
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
||||
@@ -8,9 +8,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/term"
|
||||
)
|
||||
|
||||
@@ -19,70 +22,86 @@ var (
|
||||
ErrNoTerminal = errors.New("no terminal")
|
||||
)
|
||||
|
||||
var (
|
||||
attrsReset = envColor("COLOR_RESET", color.Reset)
|
||||
attrsFgBlue = envColor("COLOR_BLUE", color.FgBlue)
|
||||
attrsFgGreen = envColor("COLOR_GREEN", color.FgGreen)
|
||||
attrsFgCyan = envColor("COLOR_CYAN", color.FgCyan)
|
||||
attrsFgYellow = envColor("COLOR_YELLOW", color.FgYellow)
|
||||
attrsFgMagenta = envColor("COLOR_MAGENTA", color.FgMagenta)
|
||||
attrsFgRed = envColor("COLOR_RED", color.FgRed)
|
||||
attrsFgHiBlue = envColor("COLOR_BRIGHT_BLUE", color.FgHiBlue)
|
||||
attrsFgHiGreen = envColor("COLOR_BRIGHT_GREEN", color.FgHiGreen)
|
||||
attrsFgHiCyan = envColor("COLOR_BRIGHT_CYAN", color.FgHiCyan)
|
||||
attrsFgHiYellow = envColor("COLOR_BRIGHT_YELLOW", color.FgHiYellow)
|
||||
attrsFgHiMagenta = envColor("COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)
|
||||
attrsFgHiRed = envColor("COLOR_BRIGHT_RED", color.FgHiRed)
|
||||
)
|
||||
|
||||
type (
|
||||
Color func() PrintFunc
|
||||
PrintFunc func(io.Writer, string, ...any)
|
||||
)
|
||||
|
||||
func Default() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_RESET", color.Reset)...).FprintfFunc()
|
||||
return color.New(attrsReset...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Blue() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BLUE", color.FgBlue)...).FprintfFunc()
|
||||
return color.New(attrsFgBlue...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Green() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_GREEN", color.FgGreen)...).FprintfFunc()
|
||||
return color.New(attrsFgGreen...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Cyan() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_CYAN", color.FgCyan)...).FprintfFunc()
|
||||
return color.New(attrsFgCyan...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Yellow() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_YELLOW", color.FgYellow)...).FprintfFunc()
|
||||
return color.New(attrsFgYellow...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Magenta() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_MAGENTA", color.FgMagenta)...).FprintfFunc()
|
||||
return color.New(attrsFgMagenta...).FprintfFunc()
|
||||
}
|
||||
|
||||
func Red() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
|
||||
return color.New(attrsFgRed...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightBlue() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
|
||||
return color.New(attrsFgHiBlue...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightGreen() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
|
||||
return color.New(attrsFgHiGreen...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightCyan() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
|
||||
return color.New(attrsFgHiCyan...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightYellow() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
|
||||
return color.New(attrsFgHiYellow...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightMagenta() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
|
||||
return color.New(attrsFgHiMagenta...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightRed() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
|
||||
return color.New(attrsFgHiRed...).FprintfFunc()
|
||||
}
|
||||
|
||||
func envColor(env string, defaultColor color.Attribute) []color.Attribute {
|
||||
func envColor(name string, defaultColor color.Attribute) []color.Attribute {
|
||||
if os.Getenv("FORCE_COLOR") != "" {
|
||||
color.NoColor = false
|
||||
}
|
||||
|
||||
// Fetch the environment variable
|
||||
override := os.Getenv(env)
|
||||
override := env.GetTaskEnv(name)
|
||||
|
||||
// First, try splitting the string by commas (RGB shortcut syntax) and if it
|
||||
// matches, then prepend the 256-color foreground escape sequence.
|
||||
@@ -195,3 +214,16 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logger) PrintExperiments() error {
|
||||
w := tabwriter.NewWriter(l.Stdout, 0, 8, 0, ' ', 0)
|
||||
for _, x := range experiments.List() {
|
||||
if !x.Active() {
|
||||
continue
|
||||
}
|
||||
l.FOutf(w, Yellow, "* ")
|
||||
l.FOutf(w, Green, x.Name)
|
||||
l.FOutf(w, Default, ": \t%s\n", x.String())
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
// Code generated by mockery v2.24.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
ast "github.com/go-task/task/v3/taskfile/ast"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// SourcesCheckable is an autogenerated mock type for the SourcesCheckable type
|
||||
type SourcesCheckable struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type SourcesCheckable_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *SourcesCheckable) EXPECT() *SourcesCheckable_Expecter {
|
||||
return &SourcesCheckable_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// IsUpToDate provides a mock function with given fields: t
|
||||
func (_m *SourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
ret := _m.Called(t)
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok {
|
||||
return rf(t)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*ast.Task) bool); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*ast.Task) error); ok {
|
||||
r1 = rf(t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
|
||||
type SourcesCheckable_IsUpToDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsUpToDate is a helper method to define mock.On call
|
||||
// - t *ast.Task
|
||||
func (_e *SourcesCheckable_Expecter) IsUpToDate(t interface{}) *SourcesCheckable_IsUpToDate_Call {
|
||||
return &SourcesCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", t)}
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *SourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_IsUpToDate_Call) Return(_a0 bool, _a1 error) *SourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(*ast.Task) (bool, error)) *SourcesCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Kind provides a mock function with given fields:
|
||||
func (_m *SourcesCheckable) Kind() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind'
|
||||
type SourcesCheckable_Kind_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Kind is a helper method to define mock.On call
|
||||
func (_e *SourcesCheckable_Expecter) Kind() *SourcesCheckable_Kind_Call {
|
||||
return &SourcesCheckable_Kind_Call{Call: _e.mock.On("Kind")}
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Kind_Call) Run(run func()) *SourcesCheckable_Kind_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Kind_Call) Return(_a0 string) *SourcesCheckable_Kind_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Kind_Call) RunAndReturn(run func() string) *SourcesCheckable_Kind_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnError provides a mock function with given fields: t
|
||||
func (_m *SourcesCheckable) OnError(t *ast.Task) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*ast.Task) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError'
|
||||
type SourcesCheckable_OnError_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnError is a helper method to define mock.On call
|
||||
// - t *ast.Task
|
||||
func (_e *SourcesCheckable_Expecter) OnError(t interface{}) *SourcesCheckable_OnError_Call {
|
||||
return &SourcesCheckable_OnError_Call{Call: _e.mock.On("OnError", t)}
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *SourcesCheckable_OnError_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_OnError_Call) Return(_a0 error) *SourcesCheckable_OnError_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_OnError_Call) RunAndReturn(run func(*ast.Task) error) *SourcesCheckable_OnError_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Value provides a mock function with given fields: t
|
||||
func (_m *SourcesCheckable) Value(t *ast.Task) (interface{}, error) {
|
||||
ret := _m.Called(t)
|
||||
|
||||
var r0 interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*ast.Task) (interface{}, error)); ok {
|
||||
return rf(t)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*ast.Task) interface{}); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*ast.Task) error); ok {
|
||||
r1 = rf(t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'
|
||||
type SourcesCheckable_Value_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Value is a helper method to define mock.On call
|
||||
// - t *ast.Task
|
||||
func (_e *SourcesCheckable_Expecter) Value(t interface{}) *SourcesCheckable_Value_Call {
|
||||
return &SourcesCheckable_Value_Call{Call: _e.mock.On("Value", t)}
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *SourcesCheckable_Value_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Value_Call) Return(_a0 interface{}, _a1 error) *SourcesCheckable_Value_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SourcesCheckable_Value_Call) RunAndReturn(run func(*ast.Task) (interface{}, error)) *SourcesCheckable_Value_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewSourcesCheckable interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewSourcesCheckable creates a new instance of SourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewSourcesCheckable(t mockConstructorTestingTNewSourcesCheckable) *SourcesCheckable {
|
||||
mock := &SourcesCheckable{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// Code generated by mockery v2.24.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
ast "github.com/go-task/task/v3/taskfile/ast"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// StatusCheckable is an autogenerated mock type for the StatusCheckable type
|
||||
type StatusCheckable struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type StatusCheckable_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *StatusCheckable) EXPECT() *StatusCheckable_Expecter {
|
||||
return &StatusCheckable_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// IsUpToDate provides a mock function with given fields: ctx, t
|
||||
func (_m *StatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {
|
||||
ret := _m.Called(ctx, t)
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok {
|
||||
return rf(ctx, t)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok {
|
||||
r0 = rf(ctx, t)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok {
|
||||
r1 = rf(ctx, t)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'
|
||||
type StatusCheckable_IsUpToDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsUpToDate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - t *ast.Task
|
||||
func (_e *StatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *StatusCheckable_IsUpToDate_Call {
|
||||
return &StatusCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", ctx, t)}
|
||||
}
|
||||
|
||||
func (_c *StatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *StatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*ast.Task))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StatusCheckable_IsUpToDate_Call) Return(_a0 bool, _a1 error) *StatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StatusCheckable_IsUpToDate_Call) RunAndReturn(run func(context.Context, *ast.Task) (bool, error)) *StatusCheckable_IsUpToDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewStatusCheckable interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewStatusCheckable creates a new instance of StatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewStatusCheckable(t mockConstructorTestingTNewStatusCheckable) *StatusCheckable {
|
||||
mock := &StatusCheckable{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
86
internal/slicesext/slicesext_test.go
Normal file
86
internal/slicesext/slicesext_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
package sort
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type TaskSorter interface {
|
||||
Sort([]*ast.Task)
|
||||
// A Sorter is any function that sorts a set of tasks.
|
||||
type Sorter func(items []string, namespaces []string) []string
|
||||
|
||||
// NoSort leaves the tasks in the order they are defined.
|
||||
func NoSort(items []string, namespaces []string) []string {
|
||||
return items
|
||||
}
|
||||
|
||||
type Noop struct{}
|
||||
|
||||
func (s *Noop) Sort(tasks []*ast.Task) {}
|
||||
|
||||
type AlphaNumeric struct{}
|
||||
|
||||
// Tasks that are not namespaced should be listed before tasks that are.
|
||||
// We detect this by searching for a ':' in the task name.
|
||||
func (s *AlphaNumeric) Sort(tasks []*ast.Task) {
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].Task < tasks[j].Task
|
||||
})
|
||||
// AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order
|
||||
// by task name.
|
||||
func AlphaNumeric(items []string, namespaces []string) []string {
|
||||
slices.Sort(items)
|
||||
return items
|
||||
}
|
||||
|
||||
type AlphaNumericWithRootTasksFirst struct{}
|
||||
|
||||
// Tasks that are not namespaced should be listed before tasks that are.
|
||||
// We detect this by searching for a ':' in the task name.
|
||||
func (s *AlphaNumericWithRootTasksFirst) Sort(tasks []*ast.Task) {
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
iContainsColon := strings.Contains(tasks[i].Task, ":")
|
||||
jContainsColon := strings.Contains(tasks[j].Task, ":")
|
||||
// AlphaNumericWithRootTasksFirst sorts the JSON output so that tasks are in
|
||||
// alpha numeric order by task name. It will also ensure that tasks that are not
|
||||
// namespaced will be listed before tasks that are. We detect this by searching
|
||||
// for a ':' in the task name.
|
||||
func AlphaNumericWithRootTasksFirst(items []string, namespaces []string) []string {
|
||||
if len(namespaces) > 0 {
|
||||
return AlphaNumeric(items, namespaces)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
iContainsColon := strings.Contains(items[i], ":")
|
||||
jContainsColon := strings.Contains(items[j], ":")
|
||||
if iContainsColon == jContainsColon {
|
||||
return tasks[i].Task < tasks[j].Task
|
||||
return items[i] < items[j]
|
||||
}
|
||||
if !iContainsColon && jContainsColon {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -4,74 +4,110 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
|
||||
task1 := &ast.Task{Task: "task1"}
|
||||
task2 := &ast.Task{Task: "task2"}
|
||||
task3 := &ast.Task{Task: "ns1:task3"}
|
||||
task4 := &ast.Task{Task: "ns2:task4"}
|
||||
task5 := &ast.Task{Task: "task5"}
|
||||
task6 := &ast.Task{Task: "ns3:task6"}
|
||||
t.Parallel()
|
||||
|
||||
item1 := "a-item1"
|
||||
item2 := "m-item2"
|
||||
item3 := "ns1:item3"
|
||||
item4 := "ns2:item4"
|
||||
item5 := "z-item5"
|
||||
item6 := "ns3:item6"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tasks []*ast.Task
|
||||
want []*ast.Task
|
||||
items []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no namespace tasks sorted alphabetically first",
|
||||
tasks: []*ast.Task{task3, task2, task1},
|
||||
want: []*ast.Task{task1, task2, task3},
|
||||
name: "no namespace items sorted alphabetically first",
|
||||
items: []string{item3, item2, item1},
|
||||
want: []string{item1, item2, item3},
|
||||
},
|
||||
{
|
||||
name: "namespace tasks sorted alphabetically after non-namespaced tasks",
|
||||
tasks: []*ast.Task{task3, task4, task5},
|
||||
want: []*ast.Task{task5, task3, task4},
|
||||
name: "namespace items sorted alphabetically after non-namespaced items",
|
||||
items: []string{item3, item4, item5},
|
||||
want: []string{item5, item3, item4},
|
||||
},
|
||||
{
|
||||
name: "all tasks sorted alphabetically with root tasks first",
|
||||
tasks: []*ast.Task{task6, task5, task4, task3, task2, task1},
|
||||
want: []*ast.Task{task1, task2, task5, task3, task4, task6},
|
||||
name: "all items sorted alphabetically with root items first",
|
||||
items: []string{item6, item5, item4, item3, item2, item1},
|
||||
want: []string{item1, item2, item5, item3, item4, item6},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &AlphaNumericWithRootTasksFirst{}
|
||||
s.Sort(tt.tasks)
|
||||
assert.Equal(t, tt.want, tt.tasks)
|
||||
t.Parallel()
|
||||
|
||||
AlphaNumericWithRootTasksFirst(tt.items, nil)
|
||||
assert.Equal(t, tt.want, tt.items)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaNumeric_Sort(t *testing.T) {
|
||||
task1 := &ast.Task{Task: "task1"}
|
||||
task2 := &ast.Task{Task: "task2"}
|
||||
task3 := &ast.Task{Task: "ns1:task3"}
|
||||
task4 := &ast.Task{Task: "ns2:task4"}
|
||||
task5 := &ast.Task{Task: "task5"}
|
||||
task6 := &ast.Task{Task: "ns3:task6"}
|
||||
t.Parallel()
|
||||
|
||||
item1 := "a-item1"
|
||||
item2 := "m-item2"
|
||||
item3 := "ns1:item3"
|
||||
item4 := "ns2:item4"
|
||||
item5 := "z-item5"
|
||||
item6 := "ns3:item6"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tasks []*ast.Task
|
||||
want []*ast.Task
|
||||
items []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "all tasks sorted alphabetically",
|
||||
tasks: []*ast.Task{task3, task2, task5, task1, task4, task6},
|
||||
want: []*ast.Task{task3, task4, task6, task1, task2, task5},
|
||||
name: "all items sorted alphabetically",
|
||||
items: []string{item3, item2, item5, item1, item4, item6},
|
||||
want: []string{item1, item2, item3, item4, item6, item5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &AlphaNumeric{}
|
||||
s.Sort(tt.tasks)
|
||||
assert.Equal(t, tt.tasks, tt.want)
|
||||
t.Parallel()
|
||||
|
||||
AlphaNumeric(tt.items, nil)
|
||||
assert.Equal(t, tt.want, tt.items)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSort_Sort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
item1 := "a-item1"
|
||||
item2 := "m-item2"
|
||||
item3 := "ns1:item3"
|
||||
item4 := "ns2:item4"
|
||||
item5 := "z-item5"
|
||||
item6 := "ns3:item6"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
items []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "all items in order of definition",
|
||||
items: []string{item3, item2, item5, item1, item4, item6},
|
||||
want: []string{item3, item2, item5, item1, item4, item6},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NoSort(tt.items, nil)
|
||||
assert.Equal(t, tt.want, tt.items)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package templater
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"math/rand/v2"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/yaml.v3"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
@@ -17,61 +21,27 @@ var templateFuncs template.FuncMap
|
||||
|
||||
func init() {
|
||||
taskFuncs := template.FuncMap{
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"catLines": func(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
return strings.ReplaceAll(s, "\n", " ")
|
||||
},
|
||||
"splitLines": func(s string) []string {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
return strings.Split(s, "\n")
|
||||
},
|
||||
"fromSlash": func(path string) string {
|
||||
return filepath.FromSlash(path)
|
||||
},
|
||||
"toSlash": func(path string) string {
|
||||
return filepath.ToSlash(path)
|
||||
},
|
||||
"exeExt": func() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"shellQuote": func(str string) (string, error) {
|
||||
return syntax.Quote(str, syntax.LangBash)
|
||||
},
|
||||
"splitArgs": func(s string) ([]string, error) {
|
||||
return shell.Fields(s, nil)
|
||||
},
|
||||
// IsSH is deprecated.
|
||||
"IsSH": func() bool { return true },
|
||||
"joinPath": func(elem ...string) string {
|
||||
return filepath.Join(elem...)
|
||||
},
|
||||
"relPath": func(basePath, targetPath string) (string, error) {
|
||||
return filepath.Rel(basePath, targetPath)
|
||||
},
|
||||
"merge": func(base map[string]any, v ...map[string]any) map[string]any {
|
||||
cap := len(v)
|
||||
for _, m := range v {
|
||||
cap += len(m)
|
||||
}
|
||||
result := make(map[string]any, cap)
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
for _, m := range v {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
"spew": func(v any) string {
|
||||
return spew.Sdump(v)
|
||||
},
|
||||
"OS": os,
|
||||
"ARCH": arch,
|
||||
"numCPU": runtime.NumCPU,
|
||||
"catLines": catLines,
|
||||
"splitLines": splitLines,
|
||||
"fromSlash": filepath.FromSlash,
|
||||
"toSlash": filepath.ToSlash,
|
||||
"exeExt": exeExt,
|
||||
"shellQuote": shellQuote,
|
||||
"splitArgs": splitArgs,
|
||||
"IsSH": IsSH, // Deprecated
|
||||
"joinPath": filepath.Join,
|
||||
"relPath": filepath.Rel,
|
||||
"merge": merge,
|
||||
"spew": spew.Sdump,
|
||||
"fromYaml": fromYaml,
|
||||
"mustFromYaml": mustFromYaml,
|
||||
"toYaml": toYaml,
|
||||
"mustToYaml": mustToYaml,
|
||||
"uuid": uuid.New,
|
||||
"randIntN": rand.IntN,
|
||||
}
|
||||
|
||||
// aliases
|
||||
@@ -83,7 +53,80 @@ func init() {
|
||||
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
|
||||
|
||||
templateFuncs = template.FuncMap(sprig.TxtFuncMap())
|
||||
for k, v := range taskFuncs {
|
||||
templateFuncs[k] = v
|
||||
}
|
||||
maps.Copy(templateFuncs, taskFuncs)
|
||||
}
|
||||
|
||||
func os() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
func arch() string {
|
||||
return runtime.GOARCH
|
||||
}
|
||||
|
||||
func catLines(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
return strings.ReplaceAll(s, "\n", " ")
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
func exeExt() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func shellQuote(str string) (string, error) {
|
||||
return syntax.Quote(str, syntax.LangBash)
|
||||
}
|
||||
|
||||
func splitArgs(s string) ([]string, error) {
|
||||
return shell.Fields(s, nil)
|
||||
}
|
||||
|
||||
// Deprecated: now always returns true
|
||||
func IsSH() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func merge(base map[string]any, v ...map[string]any) map[string]any {
|
||||
cap := len(v)
|
||||
for _, m := range v {
|
||||
cap += len(m)
|
||||
}
|
||||
result := make(map[string]any, cap)
|
||||
maps.Copy(result, base)
|
||||
for _, m := range v {
|
||||
maps.Copy(result, m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fromYaml(v string) any {
|
||||
output, _ := mustFromYaml(v)
|
||||
return output
|
||||
}
|
||||
|
||||
func mustFromYaml(v string) (any, error) {
|
||||
var output any
|
||||
err := yaml.Unmarshal([]byte(v), &output)
|
||||
return output, err
|
||||
}
|
||||
|
||||
func toYaml(v any) string {
|
||||
output, _ := yaml.Marshal(v)
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func mustToYaml(v any) (string, error) {
|
||||
output, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/template"
|
||||
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
"github.com/go-task/template"
|
||||
)
|
||||
|
||||
// Cache is a help struct that allow us to call "replaceX" funcs multiple
|
||||
@@ -140,11 +141,10 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
|
||||
return nil
|
||||
}
|
||||
|
||||
var newVars ast.Vars
|
||||
_ = vars.Range(func(k string, v ast.Var) error {
|
||||
newVars := ast.NewVars()
|
||||
for k, v := range vars.All() {
|
||||
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return &newVars
|
||||
return newVars
|
||||
}
|
||||
|
||||
@@ -1,31 +1,67 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "embed"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
version = ""
|
||||
sum = ""
|
||||
//go:embed version.txt
|
||||
version string
|
||||
commit string
|
||||
dirty bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info.Main.Version == "" {
|
||||
version = "unknown"
|
||||
} else {
|
||||
if version == "" {
|
||||
version = info.Main.Version
|
||||
}
|
||||
sum = info.Main.Sum
|
||||
version = strings.TrimSpace(version)
|
||||
// Attempt to get build info from the Go runtime. We only use this if not
|
||||
// built from a tagged version.
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version == "(devel)" {
|
||||
commit = getCommit(info)
|
||||
dirty = getDirty(info)
|
||||
}
|
||||
}
|
||||
|
||||
func getDirty(info *debug.BuildInfo) bool {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.modified" {
|
||||
return setting.Value == "true"
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getCommit(info *debug.BuildInfo) string {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
return setting.Value[:7]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetVersion returns the version of Task. By default, this is retrieved from
|
||||
// the embedded version.txt file which is kept up-to-date by our release script.
|
||||
// However, it can also be overridden at build time using:
|
||||
// -ldflags="-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'".
|
||||
func GetVersion() string {
|
||||
return version
|
||||
}
|
||||
|
||||
func GetVersionWithSum() string {
|
||||
return fmt.Sprintf("%s (%s)", version, sum)
|
||||
// GetVersionWithBuildInfo is the same as [GetVersion], but it also includes
|
||||
// the commit hash and dirty status if available. This will only work when built
|
||||
// within inside of a Git checkout.
|
||||
func GetVersionWithBuildInfo() string {
|
||||
var buildMetadata []string
|
||||
if commit != "" {
|
||||
buildMetadata = append(buildMetadata, commit)
|
||||
}
|
||||
if dirty {
|
||||
buildMetadata = append(buildMetadata, "dirty")
|
||||
}
|
||||
if len(buildMetadata) > 0 {
|
||||
return version + "+" + strings.Join(buildMetadata, ".")
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
1
internal/version/version.txt
Normal file
1
internal/version/version.txt
Normal file
@@ -0,0 +1 @@
|
||||
3.45.2
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.39.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.26.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@go-task/go-npm": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@go-task/go-npm": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
|
||||
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw==",
|
||||
"bin": {
|
||||
"go-npm": "bin/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@go-task/go-npm": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
|
||||
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
34
package.json
34
package.json
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.39.0",
|
||||
"description": "A task runner / simpler Make alternative written in Go",
|
||||
"scripts": {
|
||||
"postinstall": "go-npm install",
|
||||
"preuninstall": "go-npm uninstall"
|
||||
},
|
||||
"goBinary": {
|
||||
"name": "task",
|
||||
"path": "./bin",
|
||||
"url": "https://github.com/go-task/task/releases/download/v{{version}}/task_{{platform}}_{{arch}}{{archive_ext}}"
|
||||
},
|
||||
"files": [],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/go-task/task.git"
|
||||
},
|
||||
"keywords": [
|
||||
"task",
|
||||
"taskfile",
|
||||
"build-tool",
|
||||
"task-runner"
|
||||
],
|
||||
"author": "Andrey Nering",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/go-task/task/issues"
|
||||
},
|
||||
"homepage": "https://taskfile.dev",
|
||||
"dependencies": {
|
||||
"@go-task/go-npm": "^0.2.0"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
51
requires.go
51
requires.go
@@ -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
|
||||
}
|
||||
|
||||
88
setup.go
88
setup.go
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/sajari/fuzzy"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
@@ -55,7 +55,7 @@ func (e *Executor) Setup() error {
|
||||
}
|
||||
|
||||
func (e *Executor) getRootNode() (taskfile.Node, error) {
|
||||
node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
|
||||
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,17 +64,28 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
|
||||
}
|
||||
|
||||
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
|
||||
defer cf()
|
||||
debugFunc := func(s string) {
|
||||
e.Logger.VerboseOutf(logger.Magenta, s)
|
||||
}
|
||||
promptFunc := func(s string) error {
|
||||
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
|
||||
}
|
||||
reader := taskfile.NewReader(
|
||||
node,
|
||||
e.Insecure,
|
||||
e.Download,
|
||||
e.Offline,
|
||||
e.Timeout,
|
||||
e.TempDir.Remote,
|
||||
e.Logger,
|
||||
taskfile.WithInsecure(e.Insecure),
|
||||
taskfile.WithDownload(e.Download),
|
||||
taskfile.WithOffline(e.Offline),
|
||||
taskfile.WithTempDir(e.TempDir.Remote),
|
||||
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
|
||||
taskfile.WithDebugFunc(debugFunc),
|
||||
taskfile.WithPromptFunc(promptFunc),
|
||||
)
|
||||
graph, err := reader.Read()
|
||||
graph, err := reader.Read(ctx, node)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if e.Taskfile, err = graph.Merge(); err != nil {
|
||||
@@ -84,7 +95,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
}
|
||||
|
||||
func (e *Executor) setupFuzzyModel() {
|
||||
if e.Taskfile != nil {
|
||||
if e.Taskfile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,12 +103,12 @@ 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) {
|
||||
if task.Internal {
|
||||
continue
|
||||
}
|
||||
words = append(words, name)
|
||||
words = slices.Concat(words, task.Aliases)
|
||||
}
|
||||
|
||||
model.Train(words)
|
||||
@@ -109,13 +120,14 @@ func (e *Executor) setupTempDir() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.Getenv("TASK_TEMP_DIR") == "" {
|
||||
tempDir := env.GetTaskEnv("TEMP_DIR")
|
||||
if tempDir == "" {
|
||||
e.TempDir = TempDir{
|
||||
Remote: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
}
|
||||
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
|
||||
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
|
||||
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
|
||||
tempDir, err := execext.ExpandLiteral(tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -128,14 +140,15 @@ func (e *Executor) setupTempDir() error {
|
||||
|
||||
} else {
|
||||
e.TempDir = TempDir{
|
||||
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
|
||||
Remote: filepathext.SmartJoin(e.Dir, tempDir),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("TASK_REMOTE_DIR") != "" {
|
||||
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") {
|
||||
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR"))
|
||||
remoteDir := env.GetTaskEnv("REMOTE_DIR")
|
||||
if remoteDir != "" {
|
||||
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
|
||||
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -191,7 +204,7 @@ func (e *Executor) setupCompiler() error {
|
||||
}
|
||||
}
|
||||
|
||||
e.Compiler = &compiler.Compiler{
|
||||
e.Compiler = &Compiler{
|
||||
Dir: e.Dir,
|
||||
Entrypoint: e.Entrypoint,
|
||||
UserWorkingDir: e.UserWorkingDir,
|
||||
@@ -203,21 +216,29 @@ func (e *Executor) setupCompiler() error {
|
||||
}
|
||||
|
||||
func (e *Executor) readDotEnvFiles() error {
|
||||
if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Taskfile.Version.LessThan(ast.V3) {
|
||||
return nil
|
||||
}
|
||||
|
||||
env, err := taskfile.Dotenv(e.Compiler, e.Taskfile, e.Dir)
|
||||
vars, err := e.Compiler.GetTaskfileVariables()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = env.Range(func(key string, value ast.Var) error {
|
||||
if ok := e.Taskfile.Env.Exists(key); !ok {
|
||||
e.Taskfile.Env.Set(key, value)
|
||||
env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range env.All() {
|
||||
if _, ok := e.Taskfile.Env.Get(k); !ok {
|
||||
e.Taskfile.Env.Set(k, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -235,7 +256,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 +267,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user