mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
222 Commits
v3.38.0
...
feat/add-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be4155ab54 | ||
|
|
8bc8490485 | ||
|
|
dbe053277e | ||
|
|
37715657ae | ||
|
|
b72d1bbcfa | ||
|
|
c676b20385 | ||
|
|
16ac79c561 | ||
|
|
596fd29cb2 | ||
|
|
d3e0fc9eea | ||
|
|
08bd0d982a | ||
|
|
10d7123f60 | ||
|
|
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 | ||
|
|
1275ab1b5b | ||
|
|
0c05dcbe0f | ||
|
|
e9983e299f | ||
|
|
a450f2daea | ||
|
|
c77c8a419b | ||
|
|
a233b52c65 | ||
|
|
0e2c9cc88f | ||
|
|
dd9cec611a | ||
|
|
6985413f93 | ||
|
|
cf77768c82 | ||
|
|
6c3b13b676 | ||
|
|
ad45c7aeb3 | ||
|
|
e4b4d04abd | ||
|
|
a3bdb6c40a | ||
|
|
eb39dd94d0 | ||
|
|
21cd573770 | ||
|
|
281d259e6e | ||
|
|
1cb5daf73e | ||
|
|
3747b2ab7f | ||
|
|
d727ef5393 | ||
|
|
a72b65b3b2 | ||
|
|
ef3b853728 | ||
|
|
f302b50519 | ||
|
|
c243b0ec7e | ||
|
|
32158dac87 | ||
|
|
0a59890a46 | ||
|
|
defbcf6acd | ||
|
|
045d054a5f | ||
|
|
0941de3318 | ||
|
|
b259edeb65 | ||
|
|
35119c12ab | ||
|
|
f6ff775d11 | ||
|
|
5e9851f42f | ||
|
|
51c569ef37 | ||
|
|
1ca432a80d | ||
|
|
e781b3d4e0 | ||
|
|
81ff1cdea0 | ||
|
|
1f2cbfb932 | ||
|
|
4b6c79aca5 | ||
|
|
5739495739 | ||
|
|
9d72fa3250 | ||
|
|
4123ffc780 | ||
|
|
cdafc67bef | ||
|
|
9ee4f21d62 | ||
|
|
133086d647 |
@@ -8,6 +8,6 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
|
||||
[*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,css,svg,sh,bash,fish}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
2
.github/CODE_OF_CONDUCT.md
vendored
2
.github/CODE_OF_CONDUCT.md
vendored
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey@nering.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at task@taskfile.dev. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
||||
github: [andreynering, pd93]
|
||||
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: |
|
||||
|
||||
14
.github/workflows/issue-experiment.yml
vendored
14
.github/workflows/issue-experiment.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} proposed', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} draft', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} candidate', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} stable', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} released', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} abandoned', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} superseded', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
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: |
|
||||
|
||||
19
.github/workflows/lint.yml
vendored
19
.github/workflows/lint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.55.2
|
||||
version: v1.64.2
|
||||
|
||||
lint-jsonschema:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -41,3 +41,18 @@ jobs:
|
||||
|
||||
- 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@v45
|
||||
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.')
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.23.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
|
||||
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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,8 +24,7 @@ dist/
|
||||
|
||||
# editors
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/*-sample.json
|
||||
.vscode/settings.json
|
||||
.fleet/
|
||||
|
||||
# exuberant ctags
|
||||
|
||||
@@ -5,13 +5,32 @@
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- depguard
|
||||
- goimports
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- misspell
|
||||
- noctx
|
||||
- paralleltest
|
||||
- usetesting
|
||||
- thelper
|
||||
- tparallel
|
||||
|
||||
linters-settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
files:
|
||||
- "$all"
|
||||
- "!$test"
|
||||
- "!**/errors/*.go"
|
||||
deny:
|
||||
- pkg: "errors"
|
||||
desc: "Use github.com/go-task/task/v3/errors instead"
|
||||
goimports:
|
||||
local-prefixes: github.com/go-task
|
||||
gofumpt:
|
||||
module-path: github.com/go-task/task/v3
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- binary: task
|
||||
main: ./cmd/task
|
||||
@@ -46,7 +49,7 @@ release:
|
||||
draft: true
|
||||
|
||||
snapshot:
|
||||
name_template: "{{.Tag}}"
|
||||
version_template: "{{.Version}}"
|
||||
|
||||
checksum:
|
||||
name_template: "task_checksums.txt"
|
||||
@@ -54,7 +57,7 @@ checksum:
|
||||
nfpms:
|
||||
- vendor: Task
|
||||
homepage: https://taskfile.dev
|
||||
maintainer: Andrey Nering <andrey@nering.com.br>
|
||||
maintainer: The Task authors <task@taskfile.dev>
|
||||
description: Simple task runner written in Go
|
||||
license: MIT
|
||||
conflicts:
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"golang.go",
|
||||
"task.vscode-task"
|
||||
]
|
||||
}
|
||||
198
CHANGELOG.md
198
CHANGELOG.md
@@ -1,5 +1,183 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- 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).
|
||||
|
||||
#### 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 `v3/errors` to `v3/taskfile` (#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 `v3/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 `v3/taskfile/ast` in favour of new iterator
|
||||
functions (#1798 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
|
||||
[Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)
|
||||
(#1038, #1633 by @vmaerten).
|
||||
- Added a CI lint job to ensure that the docs are updated correctly (#1719 by
|
||||
@vmaerten).
|
||||
- Updated minimum required Go version to 1.22 (#1758 by @pd93).
|
||||
- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes
|
||||
with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).
|
||||
- Expose a new `ALIAS` special variable, which will contain the alias used to
|
||||
call the current task. Falls back to the task name. (#1764 by @DanStory).
|
||||
- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was
|
||||
absolute. (#1715 by @vmaerten).
|
||||
- Added an option to declare an included Taskfile as flattened (#1704 by
|
||||
@vmaerten).
|
||||
- Added a new
|
||||
[`--completion` flag](https://taskfile.dev/installation/#setup-completions) to
|
||||
output completion scripts for various shells (#293, #1157 by @pd93).
|
||||
- This is now the preferred way to install completions.
|
||||
- The completion scripts in the `completion` directory
|
||||
[are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).
|
||||
- Added the ability to
|
||||
[loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)
|
||||
(#1766, #1767, #1784 by @pd93).
|
||||
- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782
|
||||
by @vmaerten).
|
||||
- Fixed panic when having a flattened included Taskfile that contains a
|
||||
`default` task (#1777, #1778 by @vmaerten).
|
||||
- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).
|
||||
|
||||
## v3.38.0 - 2024-06-30
|
||||
|
||||
- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).
|
||||
@@ -218,8 +396,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).
|
||||
|
||||
@@ -278,7 +456,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
|
||||
@@ -293,7 +471,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
|
||||
@@ -366,8 +544,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
|
||||
|
||||
@@ -722,7 +900,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
|
||||
@@ -731,7 +909,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
|
||||
@@ -873,7 +1051,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
|
||||
@@ -933,7 +1111,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)
|
||||
|
||||
14
README.md
14
README.md
@@ -10,6 +10,18 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
</p>
|
||||
|
||||
<h1>Gold Sponsors</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a target="_blank" href="https://devowl.io">
|
||||
<img src="/website/static/img/devowl.io.svg" height="100px" title="devowl.io" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
45
Taskfile.yml
45
Taskfile.yml
@@ -27,7 +27,7 @@ tasks:
|
||||
- go install -v ./cmd/task
|
||||
|
||||
generate:
|
||||
desc: Runs Go generate to create mocks
|
||||
desc: Runs Mockery to create mocks
|
||||
aliases: [gen, g]
|
||||
deps: [install:mockery]
|
||||
sources:
|
||||
@@ -57,6 +57,7 @@ tasks:
|
||||
|
||||
clean:
|
||||
desc: Cleans temp files and folders
|
||||
aliases: [clear]
|
||||
cmds:
|
||||
- rm -rf dist/
|
||||
- rm -rf tmp/
|
||||
@@ -97,21 +98,17 @@ tasks:
|
||||
test:
|
||||
desc: Runs test suite
|
||||
aliases: [t]
|
||||
deps: [install]
|
||||
sources:
|
||||
- "**/*.go"
|
||||
- "testdata/**/*"
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}}
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
- go test ./...
|
||||
|
||||
test:all:
|
||||
desc: Runs test suite with signals and watch tests included
|
||||
deps: [install, sleepit:build]
|
||||
deps: [sleepit:build]
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
- go test -tags 'signals watch' ./...
|
||||
|
||||
goreleaser:test:
|
||||
desc: Tests release process without publishing
|
||||
@@ -121,7 +118,23 @@ tasks:
|
||||
goreleaser:install:
|
||||
desc: Installs goreleaser
|
||||
cmds:
|
||||
- go install github.com/goreleaser/goreleaser@latest
|
||||
- go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
gorelease:install:
|
||||
desc: "Installs gorelease: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease"
|
||||
status:
|
||||
- command -v gorelease
|
||||
cmds:
|
||||
- go install golang.org/x/exp/cmd/gorelease@latest
|
||||
|
||||
api:check:
|
||||
desc: Checks what changes have been made to the public API
|
||||
deps: [gorelease:install]
|
||||
vars:
|
||||
LATEST:
|
||||
sh: git describe --tags --abbrev=0
|
||||
cmds:
|
||||
- gorelease -base={{.LATEST}}
|
||||
|
||||
release:*:
|
||||
desc: Prepare the project for a new release
|
||||
@@ -175,11 +188,3 @@ tasks:
|
||||
desc: Publish release to npm
|
||||
cmds:
|
||||
- npm publish --access=public
|
||||
|
||||
packages:
|
||||
cmds:
|
||||
- echo '{{.GO_PACKAGES}}'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
silent: true
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// Parse parses command line argument: tasks and global variables
|
||||
func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
|
||||
calls := []*ast.Call{}
|
||||
globals := &ast.Vars{}
|
||||
globals := ast.NewVars()
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "=") {
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"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
|
||||
@@ -32,30 +33,40 @@ func TestArgs(t *testing.T) {
|
||||
{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{
|
||||
{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"},
|
||||
@@ -63,14 +74,14 @@ func TestArgs(t *testing.T) {
|
||||
{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,
|
||||
@@ -83,25 +94,32 @@ func TestArgs(t *testing.T) {
|
||||
{
|
||||
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"},
|
||||
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,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -12,6 +11,8 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/otiai10/copy"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/go-task/task/v3/args"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/flags"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
@@ -44,7 +46,7 @@ func main() {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
logger := &logger.Logger{
|
||||
log := &logger.Logger{
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Verbose: flags.Verbose,
|
||||
@@ -69,7 +71,7 @@ func run() error {
|
||||
}
|
||||
|
||||
if flags.Experiments {
|
||||
return experiments.List(logger)
|
||||
return log.PrintExperiments()
|
||||
}
|
||||
|
||||
if flags.Init {
|
||||
@@ -77,9 +79,37 @@ func run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
|
||||
args, _, err := getArgs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := wd
|
||||
if len(args) > 0 {
|
||||
name := args[0]
|
||||
if filepathext.IsExtOnly(name) {
|
||||
name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name))
|
||||
}
|
||||
path = filepathext.SmartJoin(wd, name)
|
||||
}
|
||||
finalPath, err := task.InitTaskfile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !flags.Silent {
|
||||
if flags.Verbose {
|
||||
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
|
||||
}
|
||||
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.Completion != "" {
|
||||
script, err := task.Completion(flags.Completion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(script)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,12 +121,16 @@ func run() error {
|
||||
dir = home
|
||||
}
|
||||
|
||||
var taskSorter sort.TaskSorter
|
||||
if err := experiments.Validate(); err != nil {
|
||||
log.Warnf("%s\n", err.Error())
|
||||
}
|
||||
|
||||
var taskSorter sort.Sorter
|
||||
switch flags.TaskSort {
|
||||
case "none":
|
||||
taskSorter = &sort.Noop{}
|
||||
taskSorter = nil
|
||||
case "alphanumeric":
|
||||
taskSorter = &sort.AlphaNumeric{}
|
||||
taskSorter = sort.AlphaNumeric
|
||||
}
|
||||
|
||||
e := task.Executor{
|
||||
@@ -123,8 +157,9 @@ func run() error {
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
|
||||
OutputStyle: flags.Output,
|
||||
TaskSorter: taskSorter,
|
||||
OutputStyle: flags.Output,
|
||||
TaskSorter: taskSorter,
|
||||
EnableVersionCheck: true,
|
||||
}
|
||||
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
|
||||
if err := listOptions.Validate(); err != nil {
|
||||
@@ -135,9 +170,6 @@ func run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if experiments.AnyVariables.Enabled {
|
||||
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
|
||||
}
|
||||
|
||||
// If the download flag is specified, we should stop execution as soon as
|
||||
// taskfile is downloaded
|
||||
@@ -189,6 +221,7 @@ func run() error {
|
||||
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 {
|
||||
|
||||
34
completion.go
Normal file
34
completion.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:embed completion/bash/task.bash
|
||||
var completionBash string
|
||||
|
||||
//go:embed completion/fish/task.fish
|
||||
var completionFish string
|
||||
|
||||
//go:embed completion/ps/task.ps1
|
||||
var completionPowershell string
|
||||
|
||||
//go:embed completion/zsh/_task
|
||||
var completionZsh string
|
||||
|
||||
func Completion(completion string) (string, error) {
|
||||
// Get the file extension for the selected shell
|
||||
switch completion {
|
||||
case "bash":
|
||||
return completionBash, nil
|
||||
case "fish":
|
||||
return completionFish, nil
|
||||
case "powershell":
|
||||
return completionPowershell, nil
|
||||
case "zsh":
|
||||
return completionZsh, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown shell: %s", completion)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function __task_get_tasks --description "Prints all available tasks with their d
|
||||
end
|
||||
|
||||
# Grab names and descriptions (if any) of the tasks
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(aliases.*/\1\t\2/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
|
||||
if test $output
|
||||
echo $output
|
||||
end
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
CodeTaskCalledTooManyTimes
|
||||
CodeTaskCancelled
|
||||
CodeTaskMissingRequiredVars
|
||||
CodeTaskNotAllowedVars
|
||||
)
|
||||
|
||||
// TaskError extends the standard error interface with a Code method. This code will
|
||||
|
||||
@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
|
||||
return CodeTaskNameConflict
|
||||
}
|
||||
|
||||
type TaskNameFlattenConflictError struct {
|
||||
TaskName string
|
||||
Include string
|
||||
}
|
||||
|
||||
func (err *TaskNameFlattenConflictError) Error() string {
|
||||
return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include)
|
||||
}
|
||||
|
||||
func (err *TaskNameFlattenConflictError) Code() int {
|
||||
return CodeTaskNameConflict
|
||||
}
|
||||
|
||||
// TaskCalledTooManyTimesError is returned when the maximum task call limit is
|
||||
// exceeded. This is to prevent infinite loops and cyclic dependencies.
|
||||
type TaskCalledTooManyTimesError struct {
|
||||
@@ -128,20 +141,62 @@ func (err *TaskCancelledNoTerminalError) Code() int {
|
||||
return CodeTaskCancelled
|
||||
}
|
||||
|
||||
// TaskMissingRequiredVars is returned when a task is missing required variables.
|
||||
type TaskMissingRequiredVars struct {
|
||||
// TaskMissingRequiredVarsError is returned when a task is missing required variables.
|
||||
|
||||
type MissingVar struct {
|
||||
Name string
|
||||
AllowedValues []string
|
||||
}
|
||||
type TaskMissingRequiredVarsError struct {
|
||||
TaskName string
|
||||
MissingVars []string
|
||||
MissingVars []MissingVar
|
||||
}
|
||||
|
||||
func (err *TaskMissingRequiredVars) Error() string {
|
||||
func (v MissingVar) String() string {
|
||||
if len(v.AllowedValues) == 0 {
|
||||
return v.Name
|
||||
}
|
||||
return fmt.Sprintf("%s (allowed values: %v)", v.Name, v.AllowedValues)
|
||||
}
|
||||
|
||||
func (err *TaskMissingRequiredVarsError) Error() string {
|
||||
var vars []string
|
||||
for _, v := range err.MissingVars {
|
||||
vars = append(vars, v.String())
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`task: Task %q cancelled because it is missing required variables: %s`,
|
||||
err.TaskName,
|
||||
strings.Join(err.MissingVars, ", "),
|
||||
)
|
||||
strings.Join(vars, ", "))
|
||||
}
|
||||
|
||||
func (err *TaskMissingRequiredVars) Code() int {
|
||||
func (err *TaskMissingRequiredVarsError) Code() int {
|
||||
return CodeTaskMissingRequiredVars
|
||||
}
|
||||
|
||||
type NotAllowedVar struct {
|
||||
Value string
|
||||
Enum []string
|
||||
Name string
|
||||
}
|
||||
|
||||
type TaskNotAllowedVarsError struct {
|
||||
TaskName string
|
||||
NotAllowedVars []NotAllowedVar
|
||||
}
|
||||
|
||||
func (err *TaskNotAllowedVarsError) Error() string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
|
||||
for _, s := range err.NotAllowedVars {
|
||||
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (err *TaskNotAllowedVarsError) Code() int {
|
||||
return CodeTaskNotAllowedVars
|
||||
}
|
||||
|
||||
56
go.mod
56
go.mod
@@ -1,38 +1,62 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.21.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Ladicle/tabwriter v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/alecthomas/chroma/v2 v2.14.0
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/alecthomas/chroma/v2 v2.15.0
|
||||
github.com/chainguard-dev/git-urls v1.0.2
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dominikbraun/graph v0.23.0
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.13.2
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0
|
||||
github.com/go-task/template v0.0.0-20240602015157-960e6f576656
|
||||
github.com/go-task/template v0.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-zglob v0.0.4
|
||||
github.com/mattn/go-zglob v0.0.6
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/sajari/fuzzy v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/term v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/v3 v3.8.0
|
||||
mvdan.cc/sh/v3 v3.10.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.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
170
go.sum
170
go.sum
@@ -1,39 +1,82 @@
|
||||
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.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
||||
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
|
||||
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
||||
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
|
||||
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
|
||||
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
|
||||
github.com/go-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.0.0-20240602015157-960e6f576656 h1:knZZ4zVdTBQnevBz0zSES++4Mr7wr+cHopLvHabIgkA=
|
||||
github.com/go-task/template v0.0.0-20240602015157-960e6f576656/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
|
||||
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
|
||||
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -41,44 +84,91 @@ 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.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
|
||||
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||
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/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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
|
||||
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
|
||||
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
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.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8=
|
||||
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY=
|
||||
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
|
||||
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
|
||||
|
||||
4
hash.go
4
hash.go
@@ -1,15 +1,15 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-task/task/v3/internal/hash"
|
||||
"github.com/go-task/task/v3/internal/slicesext"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func (e *Executor) GetHash(t *ast.Task) (string, error) {
|
||||
r := slicesext.FirstNonZero(t.Run, e.Taskfile.Run)
|
||||
r := cmp.Or(t.Run, e.Taskfile.Run)
|
||||
var h hash.HashFunc
|
||||
switch r {
|
||||
case "always":
|
||||
|
||||
36
help.go
36
help.go
@@ -128,18 +128,14 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
|
||||
w = e.Stdout
|
||||
}
|
||||
|
||||
// Get the list of tasks and sort them
|
||||
tasks := e.Taskfile.Tasks.Values()
|
||||
|
||||
// Sort the tasks
|
||||
if e.TaskSorter == nil {
|
||||
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
|
||||
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
|
||||
}
|
||||
e.TaskSorter.Sort(tasks)
|
||||
|
||||
// Create a list of task names
|
||||
taskNames := make([]string, 0, e.Taskfile.Tasks.Len())
|
||||
for _, task := range tasks {
|
||||
for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
|
||||
if (allTasks || task.Desc != "") && !task.Internal {
|
||||
taskNames = append(taskNames, strings.TrimRight(task.Task, ":"))
|
||||
for _, alias := range task.Aliases {
|
||||
@@ -160,23 +156,21 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
}
|
||||
var g errgroup.Group
|
||||
for i := range tasks {
|
||||
task := tasks[i]
|
||||
j := i
|
||||
aliases := []string{}
|
||||
if len(task.Aliases) > 0 {
|
||||
aliases = task.Aliases
|
||||
if len(tasks[i].Aliases) > 0 {
|
||||
aliases = tasks[i].Aliases
|
||||
}
|
||||
g.Go(func() error {
|
||||
o.Tasks[j] = editors.Task{
|
||||
Name: task.Name(),
|
||||
Desc: task.Desc,
|
||||
Summary: task.Summary,
|
||||
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: task.Location.Line,
|
||||
Column: task.Location.Column,
|
||||
Taskfile: task.Location.Taskfile,
|
||||
Line: tasks[i].Location.Line,
|
||||
Column: tasks[i].Location.Column,
|
||||
Taskfile: tasks[i].Location.Taskfile,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -186,10 +180,10 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if task.Method != "" {
|
||||
method = task.Method
|
||||
if tasks[i].Method != "" {
|
||||
method = tasks[i].Method
|
||||
}
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task,
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i],
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
@@ -199,7 +193,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
return err
|
||||
}
|
||||
|
||||
o.Tasks[j].UpToDate = upToDate
|
||||
o.Tasks[i].UpToDate = upToDate
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
38
init.go
38
init.go
@@ -1,15 +1,13 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
)
|
||||
|
||||
const defaultTaskfile = `# https://taskfile.dev
|
||||
const DefaultTaskfile = `# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
@@ -23,19 +21,31 @@ tasks:
|
||||
silent: true
|
||||
`
|
||||
|
||||
const defaultTaskfileName = "Taskfile.yml"
|
||||
const defaultTaskFilename = "Taskfile.yml"
|
||||
|
||||
// InitTaskfile Taskfile creates a new Taskfile
|
||||
func InitTaskfile(w io.Writer, dir string) error {
|
||||
f := filepathext.SmartJoin(dir, defaultTaskfileName)
|
||||
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return errors.TaskfileAlreadyExistsError{}
|
||||
// InitTaskfile creates a new Taskfile at path.
|
||||
//
|
||||
// path can be either a file path or a directory path.
|
||||
// If path is a directory, path/Taskfile.yml will be created.
|
||||
//
|
||||
// The final file path is always returned and may be different from the input path.
|
||||
func InitTaskfile(path string) (string, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err == nil && !fi.IsDir() {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(f, []byte(defaultTaskfile), 0o644); err != nil {
|
||||
return err
|
||||
if fi != nil && fi.IsDir() {
|
||||
path = filepathext.SmartJoin(path, defaultTaskFilename)
|
||||
// path was a directory, so check if Taskfile.yml exists in it
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "%s created in the current directory\n", defaultTaskfile)
|
||||
return nil
|
||||
|
||||
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
52
init_test.go
Normal file
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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -45,14 +46,12 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, err
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range specialVars {
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
specialVars, err := c.getSpecialVars(t, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range specialVars {
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
|
||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||
@@ -75,13 +74,13 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
// If the variable is not dynamic, we can set it and return
|
||||
if newVar.Value != nil || newVar.Sh == "" {
|
||||
// If the variable is already set, we can set it and return
|
||||
if newVar.Value != nil {
|
||||
result.Set(k, ast.Var{Value: newVar.Value})
|
||||
return nil
|
||||
}
|
||||
// If the variable is dynamic, we need to resolve it first
|
||||
static, err := c.HandleDynamicVar(newVar, dir)
|
||||
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,43 +103,60 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
|
||||
taskRangeFunc = getRangeFunc(dir)
|
||||
}
|
||||
|
||||
if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t != nil {
|
||||
if err := t.IncludeVars.Range(rangeFunc); err != nil {
|
||||
for k, v := range c.TaskfileEnv.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
|
||||
}
|
||||
for k, v := range c.TaskfileVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if t != nil {
|
||||
for k, v := range t.IncludeVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for k, v := range t.IncludedTaskfileVars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t == nil || call == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if err := call.Vars.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
for k, v := range call.Vars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := t.Vars.Range(taskRangeFunc); err != nil {
|
||||
return nil, err
|
||||
for k, v := range t.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) {
|
||||
c.muDynamicCache.Lock()
|
||||
defer c.muDynamicCache.Unlock()
|
||||
|
||||
// If the variable is not dynamic or it is empty, return an empty string
|
||||
if v.Sh == nil || *v.Sh == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.dynamicCache == nil {
|
||||
c.dynamicCache = make(map[string]string, 30)
|
||||
}
|
||||
if result, ok := c.dynamicCache[v.Sh]; ok {
|
||||
if result, ok := c.dynamicCache[*v.Sh]; ok {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -151,10 +167,11 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
|
||||
var stdout bytes.Buffer
|
||||
opts := &execext.RunCommandOptions{
|
||||
Command: v.Sh,
|
||||
Command: *v.Sh,
|
||||
Dir: dir,
|
||||
Stdout: &stdout,
|
||||
Stderr: c.Logger.Stderr,
|
||||
Env: e,
|
||||
}
|
||||
if err := execext.RunCommand(context.Background(), opts); err != nil {
|
||||
return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err)
|
||||
@@ -165,13 +182,13 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
result := strings.TrimSuffix(stdout.String(), "\r\n")
|
||||
result = strings.TrimSuffix(result, "\n")
|
||||
|
||||
c.dynamicCache[v.Sh] = result
|
||||
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result)
|
||||
c.dynamicCache[*v.Sh] = result
|
||||
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ResetCache clear the dymanic variables cache
|
||||
// ResetCache clear the dynamic variables cache
|
||||
func (c *Compiler) ResetCache() {
|
||||
c.muDynamicCache.Lock()
|
||||
defer c.muDynamicCache.Unlock()
|
||||
@@ -179,15 +196,23 @@ func (c *Compiler) ResetCache() {
|
||||
c.dynamicCache = nil
|
||||
}
|
||||
|
||||
func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"TASK": t.Task,
|
||||
func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
|
||||
allVars := map[string]string{
|
||||
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
||||
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
|
||||
"ROOT_DIR": c.Dir,
|
||||
"TASKFILE": t.Location.Taskfile,
|
||||
"TASKFILE_DIR": filepath.Dir(t.Location.Taskfile),
|
||||
"USER_WORKING_DIR": c.UserWorkingDir,
|
||||
"TASK_VERSION": version.GetVersion(),
|
||||
}, nil
|
||||
}
|
||||
if t != nil {
|
||||
allVars["TASK"] = t.Task
|
||||
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
|
||||
allVars["TASKFILE"] = t.Location.Taskfile
|
||||
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
|
||||
}
|
||||
if call != nil {
|
||||
allVars["ALIAS"] = call.Task
|
||||
}
|
||||
|
||||
return allVars, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// GetEnviron the all return all environment variables encapsulated on a
|
||||
// ast.Vars
|
||||
func GetEnviron() *ast.Vars {
|
||||
m := &ast.Vars{}
|
||||
m := ast.NewVars()
|
||||
for _, e := range os.Environ() {
|
||||
keyVal := strings.SplitN(e, "=", 2)
|
||||
key, val := keyVal[0], keyVal[1]
|
||||
|
||||
@@ -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
|
||||
@@ -85,7 +102,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
|
||||
|
||||
case reflect.Struct:
|
||||
// Loop over each field and call traverseFunc recursively
|
||||
for i := 0; i < v.NumField(); i += 1 {
|
||||
for i := range v.NumField() {
|
||||
if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,7 +112,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
|
||||
// Create an empty copy from the original value's type
|
||||
copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))
|
||||
// Loop over each element and call traverseFunc recursively
|
||||
for i := 0; i < v.Len(); i += 1 {
|
||||
for i := range v.Len() {
|
||||
if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
21
internal/env/env.go
vendored
21
internal/env/env.go
vendored
@@ -4,22 +4,31 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
const taskVarPrefix = "TASK_"
|
||||
|
||||
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 _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
if !experiments.EnvPrecedence.Enabled() {
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
}
|
||||
environ = append(environ, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
@@ -35,3 +44,7 @@ func isTypeAllowed(v any) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func GetTaskEnv(key string) string {
|
||||
return os.Getenv(taskVarPrefix + key)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package execext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// RunCommandOptions is the options for the RunCommand func
|
||||
@@ -90,14 +91,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
return r.Run(ctx, p)
|
||||
}
|
||||
|
||||
// IsExitError returns true the given error is an exis status error
|
||||
func IsExitError(err error) bool {
|
||||
if _, ok := interp.IsExitStatus(err); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
|
||||
// if available.
|
||||
func Expand(s string) (string, error) {
|
||||
|
||||
35
internal/experiments/errors.go
Normal file
35
internal/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,
|
||||
)
|
||||
}
|
||||
62
internal/experiments/experiment.go
Normal file
62
internal/experiments/experiment.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Experiment struct {
|
||||
Name string // The name of the experiment.
|
||||
AllowedValues []int // The values that can enable this experiment.
|
||||
Value int // The version of the experiment that is enabled.
|
||||
}
|
||||
|
||||
// New creates a new experiment with the given name and sets the values that can
|
||||
// enable it.
|
||||
func New(xName string, allowedValues ...int) Experiment {
|
||||
value := experimentConfig.Experiments[xName]
|
||||
|
||||
if value == 0 {
|
||||
value, _ = strconv.Atoi(getEnv(xName))
|
||||
}
|
||||
|
||||
x := Experiment{
|
||||
Name: xName,
|
||||
AllowedValues: allowedValues,
|
||||
Value: value,
|
||||
}
|
||||
xList = append(xList, x)
|
||||
return x
|
||||
}
|
||||
|
||||
func (x Experiment) Enabled() bool {
|
||||
return slices.Contains(x.AllowedValues, x.Value)
|
||||
}
|
||||
|
||||
func (x Experiment) Active() bool {
|
||||
return len(x.AllowedValues) > 0
|
||||
}
|
||||
|
||||
func (x Experiment) Valid() error {
|
||||
if !x.Active() && x.Value != 0 {
|
||||
return &InactiveError{
|
||||
Name: x.Name,
|
||||
}
|
||||
}
|
||||
if !x.Enabled() && x.Value != 0 {
|
||||
return &InvalidValueError{
|
||||
Name: x.Name,
|
||||
AllowedValues: x.AllowedValues,
|
||||
Value: x.Value,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x Experiment) String() string {
|
||||
if x.Enabled() {
|
||||
return fmt.Sprintf("on (%d)", x.Value)
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
75
internal/experiments/experiment_test.go
Normal file
75
internal/experiments/experiment_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package experiments_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
const (
|
||||
exampleExperiment = "EXAMPLE"
|
||||
exampleExperimentEnv = "TASK_X_EXAMPLE"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
allowedValues []int
|
||||
value int
|
||||
wantEnabled bool
|
||||
wantActive bool
|
||||
wantValid error
|
||||
}{
|
||||
{
|
||||
name: `[] allowed, value=""`,
|
||||
wantEnabled: false,
|
||||
wantActive: false,
|
||||
},
|
||||
{
|
||||
name: `[] allowed, value="1"`,
|
||||
value: 1,
|
||||
wantEnabled: false,
|
||||
wantActive: false,
|
||||
wantValid: &experiments.InactiveError{
|
||||
Name: exampleExperiment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, value=""`,
|
||||
allowedValues: []int{1},
|
||||
wantEnabled: false,
|
||||
wantActive: true,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, value="1"`,
|
||||
allowedValues: []int{1},
|
||||
value: 1,
|
||||
wantEnabled: true,
|
||||
wantActive: true,
|
||||
},
|
||||
{
|
||||
name: `[1] allowed, value="2"`,
|
||||
allowedValues: []int{1},
|
||||
value: 2,
|
||||
wantEnabled: false,
|
||||
wantActive: true,
|
||||
wantValid: &experiments.InvalidValueError{
|
||||
Name: exampleExperiment,
|
||||
AllowedValues: []int{1},
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.value))
|
||||
x := experiments.New(exampleExperiment, tt.allowedValues...)
|
||||
assert.Equal(t, exampleExperiment, x.Name)
|
||||
assert.Equal(t, tt.wantEnabled, x.Enabled())
|
||||
assert.Equal(t, tt.wantActive, x.Active())
|
||||
assert.Equal(t, tt.wantValid, x.Valid())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,60 +2,65 @@ package experiments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const envPrefix = "TASK_X_"
|
||||
|
||||
type Experiment struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Value string
|
||||
var defaultConfigFilenames = []string{
|
||||
".taskrc.yml",
|
||||
".taskrc.yaml",
|
||||
}
|
||||
|
||||
type experimentConfigFile struct {
|
||||
Experiments map[string]int `yaml:"experiments"`
|
||||
Version *semver.Version
|
||||
}
|
||||
|
||||
// A list of experiments.
|
||||
var (
|
||||
GentleForce Experiment
|
||||
RemoteTaskfiles Experiment
|
||||
AnyVariables Experiment
|
||||
MapVariables Experiment
|
||||
EnvPrecedence Experiment
|
||||
)
|
||||
|
||||
// An internal list of all the initialized experiments used for iterating.
|
||||
var (
|
||||
xList []Experiment
|
||||
experimentConfig experimentConfigFile
|
||||
)
|
||||
|
||||
func init() {
|
||||
readDotEnv()
|
||||
GentleForce = New("GENTLE_FORCE")
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES")
|
||||
AnyVariables = New("ANY_VARIABLES", "1", "2")
|
||||
MapVariables = New("MAP_VARIABLES", "1", "2")
|
||||
experimentConfig = readConfig()
|
||||
GentleForce = New("GENTLE_FORCE", 1)
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
|
||||
AnyVariables = New("ANY_VARIABLES")
|
||||
MapVariables = New("MAP_VARIABLES", 1, 2)
|
||||
EnvPrecedence = New("ENV_PRECEDENCE", 1)
|
||||
}
|
||||
|
||||
func New(xName string, enabledValues ...string) Experiment {
|
||||
if len(enabledValues) == 0 {
|
||||
enabledValues = []string{"1"}
|
||||
}
|
||||
value := getEnv(xName)
|
||||
return Experiment{
|
||||
Name: xName,
|
||||
Enabled: slices.Contains(enabledValues, value),
|
||||
Value: value,
|
||||
// Validate checks if any experiments have been enabled while being inactive.
|
||||
// If one is found, the function returns an error.
|
||||
func Validate() error {
|
||||
for _, x := range List() {
|
||||
if err := x.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x Experiment) String() string {
|
||||
if x.Enabled {
|
||||
return fmt.Sprintf("on (%s)", x.Value)
|
||||
}
|
||||
return "off"
|
||||
func List() []Experiment {
|
||||
return xList
|
||||
}
|
||||
|
||||
func getEnv(xName string) string {
|
||||
@@ -63,28 +68,29 @@ func getEnv(xName string) string {
|
||||
return os.Getenv(envName)
|
||||
}
|
||||
|
||||
func getEnvFilePath() string {
|
||||
func getFilePath(filename string) string {
|
||||
// Parse the CLI flags again to get the directory/taskfile being run
|
||||
// We use a flagset here so that we can parse a subset of flags without exiting on error.
|
||||
var dir, taskfile string
|
||||
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
||||
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
||||
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
fs.Usage = func() {}
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
// If the directory is set, find a .env file in that directory.
|
||||
if dir != "" {
|
||||
return filepath.Join(dir, ".env")
|
||||
return filepath.Join(dir, filename)
|
||||
}
|
||||
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
|
||||
if taskfile != "" {
|
||||
return filepath.Join(filepath.Dir(taskfile), ".env")
|
||||
return filepath.Join(filepath.Dir(taskfile), filename)
|
||||
}
|
||||
// Otherwise just use the current working directory.
|
||||
return ".env"
|
||||
return filename
|
||||
}
|
||||
|
||||
func readDotEnv() {
|
||||
env, _ := godotenv.Read(getEnvFilePath())
|
||||
env, _ := godotenv.Read(getFilePath(".env"))
|
||||
// If the env var is an experiment, set it.
|
||||
for key, value := range env {
|
||||
if strings.HasPrefix(key, envPrefix) {
|
||||
@@ -93,16 +99,26 @@ func readDotEnv() {
|
||||
}
|
||||
}
|
||||
|
||||
func printExperiment(w io.Writer, l *logger.Logger, x Experiment) {
|
||||
l.FOutf(w, logger.Yellow, "* ")
|
||||
l.FOutf(w, logger.Green, x.Name)
|
||||
l.FOutf(w, logger.Default, ": \t%s\n", x.String())
|
||||
}
|
||||
func readConfig() experimentConfigFile {
|
||||
var cfg experimentConfigFile
|
||||
|
||||
func List(l *logger.Logger) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0)
|
||||
printExperiment(w, l, GentleForce)
|
||||
printExperiment(w, l, RemoteTaskfiles)
|
||||
printExperiment(w, l, MapVariables)
|
||||
return w.Flush()
|
||||
var content []byte
|
||||
var err error
|
||||
for _, filename := range defaultConfigFilenames {
|
||||
path := getFilePath(filename)
|
||||
content, err = os.ReadFile(path)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return experimentConfigFile{}
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return experimentConfigFile{}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func TestNormalizeFilename(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
In, Out string
|
||||
}{
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
// | false | true | false |
|
||||
// | false | false | false |
|
||||
func TestIsTaskUpToDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *ast.Task
|
||||
@@ -150,6 +152,8 @@ func TestIsTaskUpToDate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockStatusChecker := mocks.NewStatusCheckable(t)
|
||||
if tt.setupMockStatusChecker != nil {
|
||||
tt.setupMockStatusChecker(mockStatusChecker)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"cmp"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
@@ -38,6 +41,7 @@ var (
|
||||
Version bool
|
||||
Help bool
|
||||
Init bool
|
||||
Completion string
|
||||
List bool
|
||||
ListAll bool
|
||||
ListJson bool
|
||||
@@ -76,10 +80,14 @@ func init() {
|
||||
log.Print(usage)
|
||||
pflag.PrintDefaults()
|
||||
}
|
||||
|
||||
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
|
||||
if err != nil {
|
||||
offline = false
|
||||
}
|
||||
pflag.BoolVar(&Version, "version", false, "Show Task version.")
|
||||
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
|
||||
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
|
||||
pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.")
|
||||
pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.")
|
||||
pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.")
|
||||
pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.")
|
||||
@@ -102,13 +110,13 @@ func init() {
|
||||
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
|
||||
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
|
||||
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
|
||||
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.")
|
||||
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.")
|
||||
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
|
||||
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
|
||||
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
|
||||
|
||||
// Gentle force experiment will override the force flag and add a new force-all flag
|
||||
if experiments.GentleForce.Enabled {
|
||||
if experiments.GentleForce.Enabled() {
|
||||
pflag.BoolVarP(&Force, "force", "f", false, "Forces execution of the directly called task.")
|
||||
pflag.BoolVar(&ForceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.")
|
||||
} else {
|
||||
@@ -116,9 +124,9 @@ func init() {
|
||||
}
|
||||
|
||||
// Remote Taskfiles experiment will adds the "download" and "offline" flags
|
||||
if experiments.RemoteTaskfiles.Enabled {
|
||||
if experiments.RemoteTaskfiles.Enabled() {
|
||||
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
|
||||
pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.")
|
||||
pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.")
|
||||
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
|
||||
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/term"
|
||||
)
|
||||
|
||||
@@ -19,77 +22,93 @@ 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.
|
||||
// Otherwise, split by semicolons (ANSI color codes) and use them as is.
|
||||
attributeStrs := strings.Split(override, ",")
|
||||
if len(attributeStrs) == 3 {
|
||||
attributeStrs = append([]string{"38", "2"}, attributeStrs...)
|
||||
attributeStrs = slices.Concat([]string{"38", "2"}, attributeStrs)
|
||||
} else {
|
||||
attributeStrs = strings.Split(override, ";")
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -19,12 +19,14 @@ func UniqueJoin[T cmp.Ordered](ss ...[]T) []T {
|
||||
return slices.Compact(r)
|
||||
}
|
||||
|
||||
func FirstNonZero[T comparable](values ...T) T {
|
||||
var zero T
|
||||
for _, v := range values {
|
||||
if v != zero {
|
||||
return v
|
||||
}
|
||||
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 zero
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,38 @@ package sort
|
||||
import (
|
||||
"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
|
||||
|
||||
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 {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i] < items[j]
|
||||
})
|
||||
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,78 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
|
||||
task1 := &ast.Task{Task: "task1"}
|
||||
task2 := &ast.Task{Task: "task2"}
|
||||
task3 := &ast.Task{Task: "ns1:task3"}
|
||||
task4 := &ast.Task{Task: "ns2:task4"}
|
||||
task5 := &ast.Task{Task: "task5"}
|
||||
task6 := &ast.Task{Task: "ns3:task6"}
|
||||
t.Parallel()
|
||||
|
||||
item1 := "a-item1"
|
||||
item2 := "m-item2"
|
||||
item3 := "ns1:item3"
|
||||
item4 := "ns2:item4"
|
||||
item5 := "z-item5"
|
||||
item6 := "ns3:item6"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tasks []*ast.Task
|
||||
want []*ast.Task
|
||||
items []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no namespace tasks sorted alphabetically first",
|
||||
tasks: []*ast.Task{task3, task2, task1},
|
||||
want: []*ast.Task{task1, task2, task3},
|
||||
name: "no namespace items sorted alphabetically first",
|
||||
items: []string{item3, item2, item1},
|
||||
want: []string{item1, item2, item3},
|
||||
},
|
||||
{
|
||||
name: "namespace tasks sorted alphabetically after non-namespaced tasks",
|
||||
tasks: []*ast.Task{task3, task4, task5},
|
||||
want: []*ast.Task{task5, task3, task4},
|
||||
name: "namespace items sorted alphabetically after non-namespaced items",
|
||||
items: []string{item3, item4, item5},
|
||||
want: []string{item5, item3, item4},
|
||||
},
|
||||
{
|
||||
name: "all tasks sorted alphabetically with root tasks first",
|
||||
tasks: []*ast.Task{task6, task5, task4, task3, task2, task1},
|
||||
want: []*ast.Task{task1, task2, task5, task3, task4, task6},
|
||||
name: "all items sorted alphabetically with root items first",
|
||||
items: []string{item6, item5, item4, item3, item2, item1},
|
||||
want: []string{item1, item2, item5, item3, item4, item6},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &AlphaNumericWithRootTasksFirst{}
|
||||
s.Sort(tt.tasks)
|
||||
assert.Equal(t, tt.want, tt.tasks)
|
||||
t.Parallel()
|
||||
|
||||
AlphaNumericWithRootTasksFirst(tt.items, nil)
|
||||
assert.Equal(t, tt.want, tt.items)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaNumeric_Sort(t *testing.T) {
|
||||
task1 := &ast.Task{Task: "task1"}
|
||||
task2 := &ast.Task{Task: "task2"}
|
||||
task3 := &ast.Task{Task: "ns1:task3"}
|
||||
task4 := &ast.Task{Task: "ns2:task4"}
|
||||
task5 := &ast.Task{Task: "task5"}
|
||||
task6 := &ast.Task{Task: "ns3:task6"}
|
||||
t.Parallel()
|
||||
|
||||
item1 := "a-item1"
|
||||
item2 := "m-item2"
|
||||
item3 := "ns1:item3"
|
||||
item4 := "ns2:item4"
|
||||
item5 := "z-item5"
|
||||
item6 := "ns3:item6"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tasks []*ast.Task
|
||||
want []*ast.Task
|
||||
items []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "all tasks sorted alphabetically",
|
||||
tasks: []*ast.Task{task3, task2, task5, task1, task4, task6},
|
||||
want: []*ast.Task{task3, task4, task6, task1, task2, task5},
|
||||
name: "all items sorted alphabetically",
|
||||
items: []string{item3, item2, item5, item1, item4, item6},
|
||||
want: []string{item1, item2, item3, item4, item6, item5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &AlphaNumeric{}
|
||||
s.Sort(tt.tasks)
|
||||
assert.Equal(t, tt.tasks, tt.want)
|
||||
t.Parallel()
|
||||
|
||||
AlphaNumeric(tt.items, nil)
|
||||
assert.Equal(t, tt.want, tt.items)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) {
|
||||
for i, call := range c {
|
||||
PrintSpaceBetweenSummaries(l, i)
|
||||
PrintTask(l, t.Tasks.Get(call.Task))
|
||||
if task, ok := t.Tasks.Get(call.Task); 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,13 +164,15 @@ 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)
|
||||
|
||||
@@ -17,8 +17,9 @@ var templateFuncs template.FuncMap
|
||||
|
||||
func init() {
|
||||
taskFuncs := template.FuncMap{
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"numCPU": func() int { return runtime.NumCPU() },
|
||||
"catLines": func(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
return strings.ReplaceAll(s, "\n", " ")
|
||||
|
||||
@@ -140,11 +140,10 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
|
||||
return nil
|
||||
}
|
||||
|
||||
var newVars ast.Vars
|
||||
_ = vars.Range(func(k string, v ast.Var) error {
|
||||
newVars := ast.NewVars()
|
||||
for k, v := range vars.All() {
|
||||
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return &newVars
|
||||
return newVars
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ var (
|
||||
|
||||
func init() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info.Main.Version == "" {
|
||||
if !ok || info.Main.Version == "(devel)" || info.Main.Version == "" {
|
||||
version = "unknown"
|
||||
} else {
|
||||
version = info.Main.Version
|
||||
sum = info.Main.Sum
|
||||
if version == "" {
|
||||
version = info.Main.Version
|
||||
}
|
||||
if sum == "" {
|
||||
sum = info.Main.Sum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.38.0",
|
||||
"version": "3.41.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.38.0",
|
||||
"version": "3.41.0",
|
||||
"description": "A task runner / simpler Make alternative written in Go",
|
||||
"scripts": {
|
||||
"postinstall": "go-npm install",
|
||||
@@ -22,7 +22,7 @@
|
||||
"build-tool",
|
||||
"task-runner"
|
||||
],
|
||||
"author": "Andrey Nering",
|
||||
"author": "The Task authors",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/go-task/task/issues"
|
||||
|
||||
@@ -2,8 +2,9 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"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"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
var ErrPreconditionFailed = errors.New("task: precondition not met")
|
||||
|
||||
func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *ast.Task) (bool, error) {
|
||||
for _, p := range t.Preconditions {
|
||||
for _, p := range slices.Concat(e.Taskfile.Preconditions.Values, t.Preconditions) {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: p.Sh,
|
||||
Dir: t.Dir,
|
||||
|
||||
51
requires.go
51
requires.go
@@ -1,31 +1,30 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, 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,
|
||||
}
|
||||
@@ -33,3 +32,33 @@ func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
64
setup.go
64
setup.go
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -54,7 +56,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
|
||||
}
|
||||
@@ -63,14 +65,21 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
|
||||
}
|
||||
|
||||
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
debugFunc := func(s string) {
|
||||
e.Logger.VerboseOutf(logger.Magenta, s)
|
||||
}
|
||||
promptFunc := func(s string) error {
|
||||
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
|
||||
}
|
||||
reader := taskfile.NewReader(
|
||||
node,
|
||||
e.Insecure,
|
||||
e.Download,
|
||||
e.Offline,
|
||||
e.Timeout,
|
||||
e.TempDir.Remote,
|
||||
e.Logger,
|
||||
taskfile.WithInsecure(e.Insecure),
|
||||
taskfile.WithDownload(e.Download),
|
||||
taskfile.WithOffline(e.Offline),
|
||||
taskfile.WithTimeout(e.Timeout),
|
||||
taskfile.WithTempDir(e.TempDir.Remote),
|
||||
taskfile.WithDebugFunc(debugFunc),
|
||||
taskfile.WithPromptFunc(promptFunc),
|
||||
)
|
||||
graph, err := reader.Read()
|
||||
if err != nil {
|
||||
@@ -91,12 +100,9 @@ func (e *Executor) setupFuzzyModel() {
|
||||
model.SetThreshold(1) // because we want to build grammar based on every task name
|
||||
|
||||
var words []string
|
||||
for _, taskName := range e.Taskfile.Tasks.Keys() {
|
||||
words = append(words, taskName)
|
||||
|
||||
for _, task := range e.Taskfile.Tasks.Values() {
|
||||
words = append(words, task.Aliases...)
|
||||
}
|
||||
for name, task := range e.Taskfile.Tasks.All(nil) {
|
||||
words = append(words, name)
|
||||
words = slices.Concat(words, task.Aliases)
|
||||
}
|
||||
|
||||
model.Train(words)
|
||||
@@ -108,13 +114,14 @@ func (e *Executor) setupTempDir() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.Getenv("TASK_TEMP_DIR") == "" {
|
||||
tempDir := env.GetTaskEnv("TEMP_DIR")
|
||||
if tempDir == "" {
|
||||
e.TempDir = TempDir{
|
||||
Remote: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
}
|
||||
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
|
||||
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
|
||||
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
|
||||
tempDir, err := execext.Expand(tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,14 +134,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_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
|
||||
remoteTempDir, err := execext.Expand(filepathext.SmartJoin(e.Dir, ".task"))
|
||||
remoteDir := env.GetTaskEnv("REMOTE_DIR")
|
||||
if remoteDir != "" {
|
||||
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
|
||||
remoteTempDir, err := execext.Expand(remoteDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,12 +219,11 @@ func (e *Executor) readDotEnvFiles() error {
|
||||
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)
|
||||
for k, v := range env.All() {
|
||||
if _, ok := e.Taskfile.Env.Get(k); !ok {
|
||||
e.Taskfile.Env.Set(k, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -234,7 +241,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{}
|
||||
}
|
||||
@@ -245,6 +252,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
|
||||
|
||||
15
signals.go
15
signals.go
@@ -8,24 +8,25 @@ import (
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
)
|
||||
|
||||
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, 3)
|
||||
ch := make(chan os.Signal, maxInterruptSignals)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
for i := 1; i <= 3; i++ {
|
||||
for i := range maxInterruptSignals {
|
||||
sig := <-ch
|
||||
|
||||
if i < 3 {
|
||||
e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig)
|
||||
continue
|
||||
if i+1 >= maxInterruptSignals {
|
||||
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
|
||||
os.Exit(1)
|
||||
e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
SLEEPIT, _ = filepath.Abs("./bin/sleepit")
|
||||
)
|
||||
var SLEEPIT, _ = filepath.Abs("./bin/sleepit")
|
||||
|
||||
func TestSignalSentToProcessGroup(t *testing.T) {
|
||||
task, err := getTaskPath()
|
||||
@@ -147,7 +145,7 @@ func TestSignalSentToProcessGroup(t *testing.T) {
|
||||
// where the negative PID means the corresponding process group. Note that
|
||||
// this negative PID works only as long as the caller of the kill(2) system
|
||||
// call has a different PID, which is the case for this test.
|
||||
for i := 1; i <= tc.sendSigs; i++ {
|
||||
for range tc.sendSigs - 1 {
|
||||
if err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil {
|
||||
t.Fatalf("sending INT signal to the process group: %v", err)
|
||||
}
|
||||
|
||||
105
task.go
105
task.go
@@ -11,6 +11,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
@@ -68,12 +70,13 @@ type Executor struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
Logger *logger.Logger
|
||||
Compiler *compiler.Compiler
|
||||
Output output.Output
|
||||
OutputStyle ast.Output
|
||||
TaskSorter sort.TaskSorter
|
||||
UserWorkingDir string
|
||||
Logger *logger.Logger
|
||||
Compiler *compiler.Compiler
|
||||
Output output.Output
|
||||
OutputStyle ast.Output
|
||||
TaskSorter sort.Sorter
|
||||
UserWorkingDir string
|
||||
EnableVersionCheck bool
|
||||
|
||||
fuzzyModel *fuzzy.Model
|
||||
|
||||
@@ -174,10 +177,19 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err = e.CompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {
|
||||
return &errors.TaskCalledTooManyTimesError{
|
||||
TaskName: t.Task,
|
||||
@@ -200,10 +212,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -233,13 +241,15 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
}
|
||||
}
|
||||
|
||||
if t.Prompt != "" && !e.Dry {
|
||||
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
|
||||
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
|
||||
} else if errors.Is(err, logger.ErrPromptCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: call.Task}
|
||||
} else if err != nil {
|
||||
return err
|
||||
for _, p := range t.Prompt {
|
||||
if p != "" && !e.Dry {
|
||||
if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
|
||||
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
|
||||
} else if errors.Is(err, logger.ErrPromptCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: call.Task}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,9 +257,11 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
|
||||
}
|
||||
|
||||
var deferredExitCode uint8
|
||||
|
||||
for i := range t.Cmds {
|
||||
if t.Cmds[i].Defer {
|
||||
defer e.runDeferred(t, call, i)
|
||||
defer e.runDeferred(t, call, i, &deferredExitCode)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -258,9 +270,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
|
||||
}
|
||||
|
||||
if execext.IsExitError(err) && t.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
|
||||
continue
|
||||
exitCode, isExitError := interp.IsExitStatus(err)
|
||||
if isExitError {
|
||||
if t.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
|
||||
continue
|
||||
}
|
||||
deferredExitCode = exitCode
|
||||
}
|
||||
|
||||
if call.Indirect {
|
||||
@@ -312,10 +328,26 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) {
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
origTask, err := e.GetTask(call)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := t.Cmds[i]
|
||||
vars, _ := e.Compiler.GetVariables(origTask, call)
|
||||
cache := &templater.Cache{Vars: vars}
|
||||
extra := map[string]any{}
|
||||
|
||||
if deferredExitCode != nil && *deferredExitCode > 0 {
|
||||
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
|
||||
}
|
||||
|
||||
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
||||
|
||||
if err := e.runCommand(ctx, t, call, i); err != nil {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
|
||||
}
|
||||
@@ -357,7 +389,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
|
||||
if err != nil {
|
||||
return fmt.Errorf("task: failed to get variables: %w", err)
|
||||
}
|
||||
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
|
||||
stdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
|
||||
|
||||
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.Cmd,
|
||||
@@ -369,10 +401,10 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
})
|
||||
if closeErr := close(err); closeErr != nil {
|
||||
if closeErr := closer(err); closeErr != nil {
|
||||
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
|
||||
}
|
||||
if execext.IsExitError(err) && cmd.IgnoreError {
|
||||
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
|
||||
return nil
|
||||
}
|
||||
@@ -425,7 +457,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
|
||||
case 0: // Carry on
|
||||
case 1:
|
||||
if call.Vars == nil {
|
||||
call.Vars = &ast.Vars{}
|
||||
call.Vars = ast.NewVars()
|
||||
}
|
||||
call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
|
||||
return matchingTasks[0].Task, nil
|
||||
@@ -443,7 +475,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
|
||||
// If didn't find one, search for a task with a matching alias
|
||||
var matchingTask *ast.Task
|
||||
var aliasedTasks []string
|
||||
for _, task := range e.Taskfile.Tasks.Values() {
|
||||
for task := range e.Taskfile.Tasks.Values(nil) {
|
||||
if slices.Contains(task.Aliases, call.Task) {
|
||||
aliasedTasks = append(aliasedTasks, task.Task)
|
||||
matchingTask = task
|
||||
@@ -479,8 +511,13 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
|
||||
// Create an error group to wait for each task to be compiled
|
||||
var g errgroup.Group
|
||||
|
||||
// Sort the tasks
|
||||
if e.TaskSorter == nil {
|
||||
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
|
||||
}
|
||||
|
||||
// Filter tasks based on the given filter functions
|
||||
for _, task := range e.Taskfile.Tasks.Values() {
|
||||
for task := range e.Taskfile.Tasks.Values(e.TaskSorter) {
|
||||
var shouldFilter bool
|
||||
for _, filter := range filters {
|
||||
if filter(task) {
|
||||
@@ -494,14 +531,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
|
||||
|
||||
// Compile the list of tasks
|
||||
for i := range tasks {
|
||||
idx := i
|
||||
task := tasks[idx]
|
||||
g.Go(func() error {
|
||||
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: task.Task})
|
||||
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tasks[idx] = compiledTask
|
||||
tasks[i] = compiledTask
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -511,12 +546,6 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort the tasks
|
||||
if e.TaskSorter == nil {
|
||||
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
|
||||
}
|
||||
e.TaskSorter.Sort(tasks)
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
|
||||
1026
task_test.go
1026
task_test.go
File diff suppressed because it is too large
Load Diff
@@ -51,18 +51,53 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
|
||||
// A command with additional options
|
||||
var cmdStruct struct {
|
||||
Cmd string
|
||||
Task string
|
||||
For *For
|
||||
Silent bool
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Defer *Defer
|
||||
Platforms []*Platform
|
||||
}
|
||||
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
|
||||
if err := node.Decode(&cmdStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if cmdStruct.Defer != nil {
|
||||
|
||||
// A deferred command
|
||||
if cmdStruct.Defer.Cmd != "" {
|
||||
c.Defer = true
|
||||
c.Cmd = cmdStruct.Defer.Cmd
|
||||
c.Silent = cmdStruct.Silent
|
||||
return nil
|
||||
}
|
||||
|
||||
// A deferred task call
|
||||
if cmdStruct.Defer.Task != "" {
|
||||
c.Defer = true
|
||||
c.Task = cmdStruct.Defer.Task
|
||||
c.Vars = cmdStruct.Defer.Vars
|
||||
c.Silent = cmdStruct.Defer.Silent
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A task call
|
||||
if cmdStruct.Task != "" {
|
||||
c.Task = cmdStruct.Task
|
||||
c.Vars = cmdStruct.Vars
|
||||
c.For = cmdStruct.For
|
||||
c.Silent = cmdStruct.Silent
|
||||
return nil
|
||||
}
|
||||
|
||||
// A command with additional options
|
||||
if cmdStruct.Cmd != "" {
|
||||
c.Cmd = cmdStruct.Cmd
|
||||
c.For = cmdStruct.For
|
||||
c.Silent = cmdStruct.Silent
|
||||
@@ -73,42 +108,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A deferred command
|
||||
var deferredCmd struct {
|
||||
Defer string
|
||||
}
|
||||
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
|
||||
c.Defer = true
|
||||
c.Cmd = deferredCmd.Defer
|
||||
return nil
|
||||
}
|
||||
|
||||
// A deferred task call
|
||||
var deferredCall struct {
|
||||
Defer Call
|
||||
}
|
||||
if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
|
||||
c.Defer = true
|
||||
c.Task = deferredCall.Defer.Task
|
||||
c.Vars = deferredCall.Defer.Vars
|
||||
return nil
|
||||
}
|
||||
|
||||
// A task call
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
For *For
|
||||
Silent bool
|
||||
}
|
||||
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
|
||||
c.Task = taskCall.Task
|
||||
c.Vars = taskCall.Vars
|
||||
c.For = taskCall.For
|
||||
c.Silent = taskCall.Silent
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
|
||||
}
|
||||
|
||||
|
||||
45
taskfile/ast/defer.go
Normal file
45
taskfile/ast/defer.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
type Defer struct {
|
||||
Cmd string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
}
|
||||
|
||||
func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
d.Cmd = cmd
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var deferStruct struct {
|
||||
Defer string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
}
|
||||
if err := node.Decode(&deferStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
d.Cmd = deferStruct.Defer
|
||||
d.Task = deferStruct.Task
|
||||
d.Vars = deferStruct.Vars
|
||||
d.Silent = deferStruct.Silent
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer")
|
||||
}
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
)
|
||||
|
||||
type For struct {
|
||||
From string
|
||||
List []any
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
From string
|
||||
List []any
|
||||
Matrix *Matrix
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
}
|
||||
|
||||
func (f *For) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -36,16 +37,21 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
case yaml.MappingNode:
|
||||
var forStruct struct {
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
Matrix *Matrix
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
}
|
||||
if err := node.Decode(&forStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if forStruct.Var == "" {
|
||||
if forStruct.Var == "" && forStruct.Matrix.Len() == 0 {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for")
|
||||
}
|
||||
if forStruct.Var != "" && forStruct.Matrix.Len() != 0 {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("cannot use both var and matrix in for")
|
||||
}
|
||||
f.Matrix = forStruct.Matrix
|
||||
f.Var = forStruct.Var
|
||||
f.Split = forStruct.Split
|
||||
f.As = forStruct.As
|
||||
@@ -60,10 +66,11 @@ func (f *For) DeepCopy() *For {
|
||||
return nil
|
||||
}
|
||||
return &For{
|
||||
From: f.From,
|
||||
List: deepcopy.Slice(f.List),
|
||||
Var: f.Var,
|
||||
Split: f.Split,
|
||||
As: f.As,
|
||||
From: f.From,
|
||||
List: deepcopy.Slice(f.List),
|
||||
Matrix: f.Matrix.DeepCopy(),
|
||||
Var: f.Var,
|
||||
Split: f.Split,
|
||||
As: f.As,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,5 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = rootVertex.Taskfile.Tasks.Range(func(name string, task *Task) error {
|
||||
if task == nil {
|
||||
task = &Task{}
|
||||
rootVertex.Taskfile.Tasks.Set(name, task)
|
||||
}
|
||||
task.Task = name
|
||||
return nil
|
||||
})
|
||||
|
||||
return rootVertex.Taskfile, nil
|
||||
}
|
||||
|
||||
@@ -1,45 +1,141 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
omap "github.com/go-task/task/v3/internal/omap"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Include represents information about included taskfiles
|
||||
type Include struct {
|
||||
Namespace string
|
||||
Taskfile string
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Aliases []string
|
||||
AdvancedImport bool
|
||||
Vars *Vars
|
||||
type (
|
||||
// Include represents information about included taskfiles
|
||||
Include struct {
|
||||
Namespace string
|
||||
Taskfile string
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Aliases []string
|
||||
Excludes []string
|
||||
AdvancedImport bool
|
||||
Vars *Vars
|
||||
Flatten bool
|
||||
}
|
||||
// Includes is an ordered map of namespaces to includes.
|
||||
Includes struct {
|
||||
om *orderedmap.OrderedMap[string, *Include]
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
// An IncludeElement is a key-value pair that is used for initializing an
|
||||
// Includes structure.
|
||||
IncludeElement orderedmap.Element[string, *Include]
|
||||
)
|
||||
|
||||
// NewIncludes creates a new instance of Includes and initializes it with the
|
||||
// provided set of elements, if any. The elements are added in the order they
|
||||
// are passed.
|
||||
func NewIncludes(els ...*IncludeElement) *Includes {
|
||||
includes := &Includes{
|
||||
om: orderedmap.NewOrderedMap[string, *Include](),
|
||||
}
|
||||
for _, el := range els {
|
||||
includes.Set(el.Key, el.Value)
|
||||
}
|
||||
return includes
|
||||
}
|
||||
|
||||
// Includes represents information about included tasksfiles
|
||||
type Includes struct {
|
||||
omap.OrderedMap[string, *Include]
|
||||
// Len returns the number of includes in the Includes map.
|
||||
func (includes *Includes) Len() int {
|
||||
if includes == nil || includes.om == nil {
|
||||
return 0
|
||||
}
|
||||
defer includes.mutex.RUnlock()
|
||||
includes.mutex.RLock()
|
||||
return includes.om.Len()
|
||||
}
|
||||
|
||||
// Get returns the value the the include with the provided key and a boolean
|
||||
// that indicates if the value was found or not. If the value is not found, the
|
||||
// returned include is a zero value and the bool is false.
|
||||
func (includes *Includes) Get(key string) (*Include, bool) {
|
||||
if includes == nil || includes.om == nil {
|
||||
return &Include{}, false
|
||||
}
|
||||
defer includes.mutex.RUnlock()
|
||||
includes.mutex.RLock()
|
||||
return includes.om.Get(key)
|
||||
}
|
||||
|
||||
// Set sets the value of the include with the provided key to the provided
|
||||
// value. If the include already exists, its value is updated. If the include
|
||||
// does not exist, it is created.
|
||||
func (includes *Includes) Set(key string, value *Include) bool {
|
||||
if includes == nil {
|
||||
includes = NewIncludes()
|
||||
}
|
||||
if includes.om == nil {
|
||||
includes.om = orderedmap.NewOrderedMap[string, *Include]()
|
||||
}
|
||||
defer includes.mutex.Unlock()
|
||||
includes.mutex.Lock()
|
||||
return includes.om.Set(key, value)
|
||||
}
|
||||
|
||||
// All returns an iterator that loops over all task key-value pairs.
|
||||
// Range calls the provided function for each include in the map. The function
|
||||
// receives the include's key and value as arguments. If the function returns
|
||||
// an error, the iteration stops and the error is returned.
|
||||
func (includes *Includes) All() iter.Seq2[string, *Include] {
|
||||
if includes == nil || includes.om == nil {
|
||||
return func(yield func(string, *Include) bool) {}
|
||||
}
|
||||
return includes.om.AllFromFront()
|
||||
}
|
||||
|
||||
// Keys returns an iterator that loops over all task keys.
|
||||
func (includes *Includes) Keys() iter.Seq[string] {
|
||||
if includes == nil || includes.om == nil {
|
||||
return func(yield func(string) bool) {}
|
||||
}
|
||||
return includes.om.Keys()
|
||||
}
|
||||
|
||||
// Values returns an iterator that loops over all task values.
|
||||
func (includes *Includes) Values() iter.Seq[*Include] {
|
||||
if includes == nil || includes.om == nil {
|
||||
return func(yield func(*Include) bool) {}
|
||||
}
|
||||
return includes.om.Values()
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
|
||||
if includes == nil || includes.om == nil {
|
||||
*includes = *NewIncludes()
|
||||
}
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
// NOTE(@andreynering): on this style of custom unmarshalling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
|
||||
// the map manually. We increment over 2 values at a time and assign
|
||||
// them as a key-value pair.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
// Decode the value node into an Include struct
|
||||
var v Include
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
// Set the include namespace
|
||||
v.Namespace = keyNode.Value
|
||||
|
||||
// Add the include to the ordered map
|
||||
includes.Set(keyNode.Value, &v)
|
||||
}
|
||||
return nil
|
||||
@@ -48,22 +144,6 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
|
||||
}
|
||||
|
||||
// Len returns the length of the map
|
||||
func (includes *Includes) Len() int {
|
||||
if includes == nil {
|
||||
return 0
|
||||
}
|
||||
return includes.OrderedMap.Len()
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
|
||||
func (includes *Includes) Range(f func(k string, v *Include) error) error {
|
||||
if includes == nil {
|
||||
return nil
|
||||
}
|
||||
return includes.OrderedMap.Range(f)
|
||||
}
|
||||
|
||||
func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
@@ -81,7 +161,9 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Flatten bool
|
||||
Aliases []string
|
||||
Excludes []string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := node.Decode(&includedTaskfile); err != nil {
|
||||
@@ -92,8 +174,10 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
include.Optional = includedTaskfile.Optional
|
||||
include.Internal = includedTaskfile.Internal
|
||||
include.Aliases = includedTaskfile.Aliases
|
||||
include.Excludes = includedTaskfile.Excludes
|
||||
include.AdvancedImport = true
|
||||
include.Vars = includedTaskfile.Vars
|
||||
include.Flatten = includedTaskfile.Flatten
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,7 +196,9 @@ func (include *Include) DeepCopy() *Include {
|
||||
Dir: include.Dir,
|
||||
Optional: include.Optional,
|
||||
Internal: include.Internal,
|
||||
Excludes: deepcopy.Slice(include.Excludes),
|
||||
AdvancedImport: include.AdvancedImport,
|
||||
Vars: include.Vars.DeepCopy(),
|
||||
Flatten: include.Flatten,
|
||||
}
|
||||
}
|
||||
|
||||
109
taskfile/ast/matrix.go
Normal file
109
taskfile/ast/matrix.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
type Matrix struct {
|
||||
om *orderedmap.OrderedMap[string, []any]
|
||||
}
|
||||
|
||||
type MatrixElement orderedmap.Element[string, []any]
|
||||
|
||||
func NewMatrix(els ...*MatrixElement) *Matrix {
|
||||
matrix := &Matrix{
|
||||
om: orderedmap.NewOrderedMap[string, []any](),
|
||||
}
|
||||
for _, el := range els {
|
||||
matrix.Set(el.Key, el.Value)
|
||||
}
|
||||
return matrix
|
||||
}
|
||||
|
||||
func (matrix *Matrix) Len() int {
|
||||
if matrix == nil || matrix.om == nil {
|
||||
return 0
|
||||
}
|
||||
return matrix.om.Len()
|
||||
}
|
||||
|
||||
func (matrix *Matrix) Get(key string) ([]any, bool) {
|
||||
if matrix == nil || matrix.om == nil {
|
||||
return nil, false
|
||||
}
|
||||
return matrix.om.Get(key)
|
||||
}
|
||||
|
||||
func (matrix *Matrix) Set(key string, value []any) bool {
|
||||
if matrix == nil {
|
||||
matrix = NewMatrix()
|
||||
}
|
||||
if matrix.om == nil {
|
||||
matrix.om = orderedmap.NewOrderedMap[string, []any]()
|
||||
}
|
||||
return matrix.om.Set(key, value)
|
||||
}
|
||||
|
||||
// All returns an iterator that loops over all task key-value pairs.
|
||||
func (matrix *Matrix) All() iter.Seq2[string, []any] {
|
||||
if matrix == nil || matrix.om == nil {
|
||||
return func(yield func(string, []any) bool) {}
|
||||
}
|
||||
return matrix.om.AllFromFront()
|
||||
}
|
||||
|
||||
// Keys returns an iterator that loops over all task keys.
|
||||
func (matrix *Matrix) Keys() iter.Seq[string] {
|
||||
if matrix == nil || matrix.om == nil {
|
||||
return func(yield func(string) bool) {}
|
||||
}
|
||||
return matrix.om.Keys()
|
||||
}
|
||||
|
||||
// Values returns an iterator that loops over all task values.
|
||||
func (matrix *Matrix) Values() iter.Seq[[]any] {
|
||||
if matrix == nil || matrix.om == nil {
|
||||
return func(yield func([]any) bool) {}
|
||||
}
|
||||
return matrix.om.Values()
|
||||
}
|
||||
|
||||
func (matrix *Matrix) DeepCopy() *Matrix {
|
||||
if matrix == nil {
|
||||
return nil
|
||||
}
|
||||
return &Matrix{
|
||||
om: deepcopy.OrderedMap(matrix.om),
|
||||
}
|
||||
}
|
||||
|
||||
func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
|
||||
// the map manually. We increment over 2 values at a time and assign
|
||||
// them as a key-value pair.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
// Decode the value node into a Matrix struct
|
||||
var v []any
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
// Add the task to the ordered map
|
||||
matrix.Set(keyNode.Value, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("matrix")
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func TestPlatformParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
Input string
|
||||
ExpectedOS string
|
||||
@@ -34,6 +36,8 @@ func TestPlatformParsing(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var p Platform
|
||||
err := p.parsePlatform(test.Input)
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
)
|
||||
|
||||
// Precondition represents a precondition necessary for a task to run
|
||||
type Precondition struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
type (
|
||||
Precondition struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Precondition) DeepCopy() *Precondition {
|
||||
if p == nil {
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
)
|
||||
|
||||
func TestPreconditionParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
content string
|
||||
v any
|
||||
|
||||
47
taskfile/ast/preconditions.go
Normal file
47
taskfile/ast/preconditions.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Precondition represents a precondition necessary for a task to run
|
||||
type (
|
||||
Preconditions struct {
|
||||
Values []*Precondition
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
func NewPreconditions() *Preconditions {
|
||||
return &Preconditions{
|
||||
Values: make([]*Precondition, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Preconditions) DeepCopy() *Preconditions {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
defer p.mutex.RUnlock()
|
||||
p.mutex.RLock()
|
||||
return &Preconditions{
|
||||
Values: deepcopy.Slice(p.Values),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Preconditions) UnmarshalYAML(node *yaml.Node) error {
|
||||
if p == nil || p.Values == nil {
|
||||
*p = *NewPreconditions()
|
||||
}
|
||||
|
||||
if err := node.Decode(&p.Values); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node).WithTypeMessage("preconditions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
29
taskfile/ast/prompt.go
Normal file
29
taskfile/ast/prompt.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
type Prompt []string
|
||||
|
||||
func (p *Prompt) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var str string
|
||||
if err := node.Decode(&str); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
*p = []string{str}
|
||||
return nil
|
||||
case yaml.SequenceNode:
|
||||
var list []string
|
||||
if err := node.Decode(&list); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
*p = list
|
||||
return nil
|
||||
}
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("prompt")
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package ast
|
||||
|
||||
import "github.com/go-task/task/v3/internal/deepcopy"
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Requires represents a set of required variables necessary for a task to run
|
||||
type Requires struct {
|
||||
Vars []string
|
||||
Vars []*VarsWithValidation
|
||||
}
|
||||
|
||||
func (r *Requires) DeepCopy() *Requires {
|
||||
@@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
|
||||
Vars: deepcopy.Slice(r.Vars),
|
||||
}
|
||||
}
|
||||
|
||||
type VarsWithValidation struct {
|
||||
Name string
|
||||
Enum []string
|
||||
}
|
||||
|
||||
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return &VarsWithValidation{
|
||||
Name: v.Name,
|
||||
Enum: v.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Name = cmd
|
||||
v.Enum = nil
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var vv struct {
|
||||
Name string
|
||||
Enum []string
|
||||
}
|
||||
if err := node.Decode(&vv); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Name = vv.Name
|
||||
v.Enum = vv.Enum
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Task struct {
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Prompt string
|
||||
Prompt Prompt
|
||||
Summary string
|
||||
Requires *Requires
|
||||
Aliases []string
|
||||
@@ -115,7 +115,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Prompt string
|
||||
Prompt Prompt
|
||||
Summary string
|
||||
Aliases []string
|
||||
Sources []*Glob
|
||||
|
||||
@@ -18,22 +18,26 @@ var V3 = semver.MustParse("3")
|
||||
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
|
||||
var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
|
||||
|
||||
// ErrIncludedTaskfilesCantHavePreconditions is returned when a included Taskfile contains Preconditions
|
||||
var ErrIncludedTaskfilesCantHavePreconditions = errors.New("task: Included Taskfiles can't have preconditions declarations. Please, move the preconditions declaration to the main Taskfile")
|
||||
|
||||
// Taskfile is the abstract syntax tree for a Taskfile
|
||||
type Taskfile struct {
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Preconditions *Preconditions
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
// Merge merges the second Taskfile into the first
|
||||
@@ -44,38 +48,50 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
if len(t2.Dotenv) > 0 {
|
||||
return ErrIncludedTaskfilesCantHaveDotenvs
|
||||
}
|
||||
if len(t2.Preconditions.Values) > 0 {
|
||||
return ErrIncludedTaskfilesCantHavePreconditions
|
||||
}
|
||||
if t2.Output.IsSet() {
|
||||
t1.Output = t2.Output
|
||||
}
|
||||
if t1.Includes == nil {
|
||||
t1.Includes = NewIncludes()
|
||||
}
|
||||
if t1.Vars == nil {
|
||||
t1.Vars = &Vars{}
|
||||
t1.Vars = NewVars()
|
||||
}
|
||||
if t1.Env == nil {
|
||||
t1.Env = &Vars{}
|
||||
t1.Env = NewVars()
|
||||
}
|
||||
if t1.Tasks == nil {
|
||||
t1.Tasks = NewTasks()
|
||||
}
|
||||
if t1.Preconditions == nil {
|
||||
t1.Preconditions = NewPreconditions()
|
||||
}
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
return nil
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
|
||||
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
var taskfile struct {
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Preconditions *Preconditions
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
}
|
||||
if err := node.Decode(&taskfile); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -93,11 +109,21 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
tf.Run = taskfile.Run
|
||||
tf.Interval = taskfile.Interval
|
||||
tf.Preconditions = taskfile.Preconditions
|
||||
if tf.Includes == nil {
|
||||
tf.Includes = NewIncludes()
|
||||
}
|
||||
if tf.Vars == nil {
|
||||
tf.Vars = &Vars{}
|
||||
tf.Vars = NewVars()
|
||||
}
|
||||
if tf.Env == nil {
|
||||
tf.Env = &Vars{}
|
||||
tf.Env = NewVars()
|
||||
}
|
||||
if tf.Tasks == nil {
|
||||
tf.Tasks = NewTasks()
|
||||
}
|
||||
if tf.Preconditions == nil {
|
||||
tf.Preconditions = NewPreconditions()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func TestCmdParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
yamlCmd = `echo "a string command"`
|
||||
yamlDep = `"task-name"`
|
||||
@@ -38,15 +39,21 @@ vars:
|
||||
yamlTaskCall,
|
||||
&ast.Cmd{},
|
||||
&ast.Cmd{
|
||||
Task: "another-task", Vars: &ast.Vars{
|
||||
OrderedMap: omap.FromMapWithOrder(
|
||||
map[string]ast.Var{
|
||||
"PARAM1": {Value: "VALUE1"},
|
||||
"PARAM2": {Value: "VALUE2"},
|
||||
Task: "another-task",
|
||||
Vars: ast.NewVars(
|
||||
&ast.VarElement{
|
||||
Key: "PARAM1",
|
||||
Value: ast.Var{
|
||||
Value: "VALUE1",
|
||||
},
|
||||
[]string{"PARAM1", "PARAM2"},
|
||||
),
|
||||
},
|
||||
},
|
||||
&ast.VarElement{
|
||||
Key: "PARAM2",
|
||||
Value: ast.Var{
|
||||
Value: "VALUE2",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,14 +65,15 @@ vars:
|
||||
yamlDeferredCall,
|
||||
&ast.Cmd{},
|
||||
&ast.Cmd{
|
||||
Task: "some_task", Vars: &ast.Vars{
|
||||
OrderedMap: omap.FromMapWithOrder(
|
||||
map[string]ast.Var{
|
||||
"PARAM1": {Value: "var"},
|
||||
Task: "some_task",
|
||||
Vars: ast.NewVars(
|
||||
&ast.VarElement{
|
||||
Key: "PARAM1",
|
||||
Value: ast.Var{
|
||||
Value: "var",
|
||||
},
|
||||
[]string{"PARAM1"},
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
Defer: true,
|
||||
},
|
||||
},
|
||||
@@ -78,15 +86,21 @@ vars:
|
||||
yamlTaskCall,
|
||||
&ast.Dep{},
|
||||
&ast.Dep{
|
||||
Task: "another-task", Vars: &ast.Vars{
|
||||
OrderedMap: omap.FromMapWithOrder(
|
||||
map[string]ast.Var{
|
||||
"PARAM1": {Value: "VALUE1"},
|
||||
"PARAM2": {Value: "VALUE2"},
|
||||
Task: "another-task",
|
||||
Vars: ast.NewVars(
|
||||
&ast.VarElement{
|
||||
Key: "PARAM1",
|
||||
Value: ast.Var{
|
||||
Value: "VALUE1",
|
||||
},
|
||||
[]string{"PARAM1", "PARAM2"},
|
||||
),
|
||||
},
|
||||
},
|
||||
&ast.VarElement{
|
||||
Key: "PARAM2",
|
||||
Value: ast.Var{
|
||||
Value: "VALUE2",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,152 +2,273 @@ package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
)
|
||||
|
||||
// Tasks represents a group of tasks
|
||||
type Tasks struct {
|
||||
omap.OrderedMap[string, *Task]
|
||||
type (
|
||||
// Tasks is an ordered map of task names to Tasks.
|
||||
Tasks struct {
|
||||
om *orderedmap.OrderedMap[string, *Task]
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
// A TaskElement is a key-value pair that is used for initializing a Tasks
|
||||
// structure.
|
||||
TaskElement orderedmap.Element[string, *Task]
|
||||
// MatchingTask represents a task that matches a given call. It includes the
|
||||
// task itself and a list of wildcards that were matched.
|
||||
MatchingTask struct {
|
||||
Task *Task
|
||||
Wildcards []string
|
||||
}
|
||||
)
|
||||
|
||||
// NewTasks creates a new instance of Tasks and initializes it with the provided
|
||||
// set of elements, if any. The elements are added in the order they are passed.
|
||||
func NewTasks(els ...*TaskElement) *Tasks {
|
||||
tasks := &Tasks{
|
||||
om: orderedmap.NewOrderedMap[string, *Task](),
|
||||
}
|
||||
for _, el := range els {
|
||||
tasks.Set(el.Key, el.Value)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
type MatchingTask struct {
|
||||
Task *Task
|
||||
Wildcards []string
|
||||
// Len returns the number of variables in the Tasks map.
|
||||
func (tasks *Tasks) Len() int {
|
||||
if tasks == nil || tasks.om == nil {
|
||||
return 0
|
||||
}
|
||||
defer tasks.mutex.RUnlock()
|
||||
tasks.mutex.RLock()
|
||||
return tasks.om.Len()
|
||||
}
|
||||
|
||||
// Get returns the value the the task with the provided key and a boolean that
|
||||
// indicates if the value was found or not. If the value is not found, the
|
||||
// returned task is a zero value and the bool is false.
|
||||
func (tasks *Tasks) Get(key string) (*Task, bool) {
|
||||
if tasks == nil || tasks.om == nil {
|
||||
return &Task{}, false
|
||||
}
|
||||
defer tasks.mutex.RUnlock()
|
||||
tasks.mutex.RLock()
|
||||
return tasks.om.Get(key)
|
||||
}
|
||||
|
||||
// Set sets the value of the task with the provided key to the provided value.
|
||||
// If the task already exists, its value is updated. If the task does not exist,
|
||||
// it is created.
|
||||
func (tasks *Tasks) Set(key string, value *Task) bool {
|
||||
if tasks == nil {
|
||||
tasks = NewTasks()
|
||||
}
|
||||
if tasks.om == nil {
|
||||
tasks.om = orderedmap.NewOrderedMap[string, *Task]()
|
||||
}
|
||||
defer tasks.mutex.Unlock()
|
||||
tasks.mutex.Lock()
|
||||
return tasks.om.Set(key, value)
|
||||
}
|
||||
|
||||
// All returns an iterator that loops over all task key-value pairs in the order
|
||||
// specified by the sorter.
|
||||
func (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] {
|
||||
if t == nil || t.om == nil {
|
||||
return func(yield func(string, *Task) bool) {}
|
||||
}
|
||||
if sorter == nil {
|
||||
return t.om.AllFromFront()
|
||||
}
|
||||
return func(yield func(string, *Task) bool) {
|
||||
for _, key := range sorter(slices.Collect(t.om.Keys()), nil) {
|
||||
el := t.om.GetElement(key)
|
||||
if !yield(el.Key, el.Value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keys returns an iterator that loops over all task keys in the order specified
|
||||
// by the sorter.
|
||||
func (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
for k := range t.All(sorter) {
|
||||
if !yield(k) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Values returns an iterator that loops over all task values in the order
|
||||
// specified by the sorter.
|
||||
func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] {
|
||||
return func(yield func(*Task) bool) {
|
||||
for _, v := range t.All(sorter) {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMatchingTasks returns a list of tasks that match the given call. A task
|
||||
// matches a call if its name is equal to the call's task name or if it matches
|
||||
// a wildcard pattern. The function returns a list of MatchingTask structs, each
|
||||
// containing a task and a list of wildcards that were matched.
|
||||
func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
|
||||
if call == nil {
|
||||
return nil
|
||||
}
|
||||
var task *Task
|
||||
var matchingTasks []*MatchingTask
|
||||
// If there is a direct match, return it
|
||||
if task = t.OrderedMap.Get(call.Task); task != nil {
|
||||
if task, ok := t.Get(call.Task); ok {
|
||||
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
|
||||
return matchingTasks
|
||||
}
|
||||
// Attempt a wildcard match
|
||||
// For now, we can just nil check the task before each loop
|
||||
_ = t.Range(func(key string, value *Task) error {
|
||||
for _, value := range t.All(nil) {
|
||||
if match, wildcards := value.WildcardMatch(call.Task); match {
|
||||
matchingTasks = append(matchingTasks, &MatchingTask{
|
||||
Task: value,
|
||||
Wildcards: wildcards,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return matchingTasks
|
||||
}
|
||||
|
||||
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
|
||||
_ = t2.Range(func(name string, v *Task) error {
|
||||
func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
|
||||
defer t2.mutex.RUnlock()
|
||||
t2.mutex.RLock()
|
||||
for name, v := range t2.All(nil) {
|
||||
// We do a deep copy of the task struct here to ensure that no data can
|
||||
// be changed elsewhere once the taskfile is merged.
|
||||
task := v.DeepCopy()
|
||||
|
||||
// Set the task to internal if EITHER the included task or the included
|
||||
// taskfile are marked as internal
|
||||
task.Internal = task.Internal || (include != nil && include.Internal)
|
||||
taskName := name
|
||||
|
||||
// Add namespaces to task dependencies
|
||||
for _, dep := range task.Deps {
|
||||
if dep != nil && dep.Task != "" {
|
||||
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
|
||||
}
|
||||
// if the task is in the exclude list, don't add it to the merged taskfile
|
||||
if slices.Contains(include.Excludes, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add namespaces to task commands
|
||||
for _, cmd := range task.Cmds {
|
||||
if cmd != nil && cmd.Task != "" {
|
||||
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task aliases
|
||||
for i, alias := range task.Aliases {
|
||||
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
|
||||
}
|
||||
|
||||
// Add namespace aliases
|
||||
if include != nil {
|
||||
for _, namespaceAlias := range include.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
|
||||
for _, alias := range v.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
|
||||
if !include.Flatten {
|
||||
// Add namespaces to task dependencies
|
||||
for _, dep := range task.Deps {
|
||||
if dep != nil && dep.Task != "" {
|
||||
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task commands
|
||||
for _, cmd := range task.Cmds {
|
||||
if cmd != nil && cmd.Task != "" {
|
||||
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task aliases
|
||||
for i, alias := range task.Aliases {
|
||||
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
|
||||
}
|
||||
|
||||
// Add namespace aliases
|
||||
if include != nil {
|
||||
for _, namespaceAlias := range include.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
|
||||
for _, alias := range v.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskName = taskNameWithNamespace(name, include.Namespace)
|
||||
task.Namespace = include.Namespace
|
||||
task.Task = taskName
|
||||
}
|
||||
|
||||
if include.AdvancedImport {
|
||||
task.Dir = filepathext.SmartJoin(include.Dir, task.Dir)
|
||||
if task.IncludeVars == nil {
|
||||
task.IncludeVars = &Vars{}
|
||||
task.IncludeVars = NewVars()
|
||||
}
|
||||
task.IncludeVars.Merge(include.Vars, nil)
|
||||
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
|
||||
}
|
||||
|
||||
if _, ok := t1.Get(taskName); ok {
|
||||
return &errors.TaskNameFlattenConflictError{
|
||||
TaskName: taskName,
|
||||
Include: include.Namespace,
|
||||
}
|
||||
}
|
||||
// Add the task to the merged taskfile
|
||||
taskNameWithNamespace := taskNameWithNamespace(name, include.Namespace)
|
||||
task.Namespace = include.Namespace
|
||||
task.Task = taskNameWithNamespace
|
||||
t1.Set(taskNameWithNamespace, task)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// If the included Taskfile has a default task and the parent namespace has
|
||||
// no task with a matching name, we can add an alias so that the user can
|
||||
// run the included Taskfile's default task without specifying its full
|
||||
// name. If the parent namespace has aliases, we add another alias for each
|
||||
// of them.
|
||||
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil {
|
||||
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
|
||||
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace)
|
||||
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Aliases...)
|
||||
t1.Set(taskName, task)
|
||||
}
|
||||
|
||||
// If the included Taskfile has a default task, is not flattened and the
|
||||
// parent namespace has no task with a matching name, we can add an alias so
|
||||
// that the user can run the included Taskfile's default task without
|
||||
// specifying its full name. If the parent namespace has aliases, we add
|
||||
// another alias for each of them.
|
||||
_, t2DefaultExists := t2.Get("default")
|
||||
_, t1NamespaceExists := t1.Get(include.Namespace)
|
||||
if t2DefaultExists && !t1NamespaceExists && !include.Flatten {
|
||||
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
|
||||
t1DefaultTask, ok := t1.Get(defaultTaskName)
|
||||
if ok {
|
||||
t1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace)
|
||||
t1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
if t == nil || t.om == nil {
|
||||
*t = *NewTasks()
|
||||
}
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
tasks := omap.New[string, *Task]()
|
||||
if err := node.Decode(&tasks); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
|
||||
// the map manually. We increment over 2 values at a time and assign
|
||||
// them as a key-value pair.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
// nolint: errcheck
|
||||
tasks.Range(func(name string, task *Task) error {
|
||||
// Set the task's name
|
||||
if task == nil {
|
||||
task = &Task{
|
||||
Task: name,
|
||||
}
|
||||
// Decode the value node into a Task struct
|
||||
var v Task
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
task.Task = name
|
||||
|
||||
// Set the task's location
|
||||
for _, keys := range node.Content {
|
||||
if keys.Value == name {
|
||||
task.Location = &Location{
|
||||
Line: keys.Line,
|
||||
Column: keys.Column,
|
||||
}
|
||||
}
|
||||
// Set the task name and location
|
||||
v.Task = keyNode.Value
|
||||
v.Location = &Location{
|
||||
Line: keyNode.Line,
|
||||
Column: keyNode.Column,
|
||||
}
|
||||
tasks.Set(name, task)
|
||||
return nil
|
||||
})
|
||||
|
||||
*t = Tasks{
|
||||
OrderedMap: tasks,
|
||||
// Add the task to the ordered map
|
||||
t.Set(keyNode.Value, &v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,90 +7,22 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
)
|
||||
|
||||
// Vars is a string[string] variables map.
|
||||
type Vars struct {
|
||||
omap.OrderedMap[string, Var]
|
||||
}
|
||||
|
||||
// ToCacheMap converts Vars to a map containing only the static
|
||||
// variables
|
||||
func (vs *Vars) ToCacheMap() (m map[string]any) {
|
||||
m = make(map[string]any, vs.Len())
|
||||
_ = vs.Range(func(k string, v Var) error {
|
||||
if v.Sh != "" {
|
||||
// Dynamic variable is not yet resolved; trigger
|
||||
// <no value> to be used in templates.
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Live != nil {
|
||||
m[k] = v.Live
|
||||
} else {
|
||||
m[k] = v.Value
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Range(f func(k string, v Var) error) error {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
return vs.OrderedMap.Range(f)
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Merge(other *Vars, include *Include) {
|
||||
if vs == nil || other == nil {
|
||||
return
|
||||
}
|
||||
_ = other.Range(func(key string, value Var) error {
|
||||
if include != nil && include.AdvancedImport {
|
||||
value.Dir = include.Dir
|
||||
}
|
||||
vs.Set(key, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors
|
||||
func (vs *Vars) Len() int {
|
||||
if vs == nil {
|
||||
return 0
|
||||
}
|
||||
return vs.OrderedMap.Len()
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Vars and copies
|
||||
// data by value from the source struct.
|
||||
func (vs *Vars) DeepCopy() *Vars {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
return &Vars{
|
||||
OrderedMap: vs.OrderedMap.DeepCopy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Var represents either a static or dynamic variable.
|
||||
type Var struct {
|
||||
Value any
|
||||
Live any
|
||||
Sh string
|
||||
Sh *string
|
||||
Ref string
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
if experiments.MapVariables.Enabled {
|
||||
if experiments.MapVariables.Enabled() {
|
||||
|
||||
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables
|
||||
if experiments.MapVariables.Value == "1" {
|
||||
if experiments.MapVariables.Value == 1 {
|
||||
var value any
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -98,7 +30,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
// If the value is a string and it starts with $, then it's a shell command
|
||||
if str, ok := value.(string); ok {
|
||||
if str, ok = strings.CutPrefix(str, "$"); ok {
|
||||
v.Sh = str
|
||||
v.Sh = &str
|
||||
return nil
|
||||
}
|
||||
if str, ok = strings.CutPrefix(str, "#"); ok {
|
||||
@@ -111,14 +43,14 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
|
||||
// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
|
||||
if experiments.MapVariables.Value == "2" {
|
||||
if experiments.MapVariables.Value == 2 {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
key := node.Content[0].Value
|
||||
switch key {
|
||||
case "sh", "ref", "map":
|
||||
var m struct {
|
||||
Sh string
|
||||
Sh *string
|
||||
Ref string
|
||||
Map any
|
||||
}
|
||||
@@ -150,7 +82,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch key {
|
||||
case "sh", "ref":
|
||||
var m struct {
|
||||
Sh string
|
||||
Sh *string
|
||||
Ref string
|
||||
}
|
||||
if err := node.Decode(&m); err != nil {
|
||||
|
||||
174
taskfile/ast/vars.go
Normal file
174
taskfile/ast/vars.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
type (
|
||||
// Vars is an ordered map of variable names to values.
|
||||
Vars struct {
|
||||
om *orderedmap.OrderedMap[string, Var]
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
// A VarElement is a key-value pair that is used for initializing a Vars
|
||||
// structure.
|
||||
VarElement orderedmap.Element[string, Var]
|
||||
)
|
||||
|
||||
// NewVars creates a new instance of Vars and initializes it with the provided
|
||||
// set of elements, if any. The elements are added in the order they are passed.
|
||||
func NewVars(els ...*VarElement) *Vars {
|
||||
vars := &Vars{
|
||||
om: orderedmap.NewOrderedMap[string, Var](),
|
||||
}
|
||||
for _, el := range els {
|
||||
vars.Set(el.Key, el.Value)
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
// Len returns the number of variables in the Vars map.
|
||||
func (vars *Vars) Len() int {
|
||||
if vars == nil || vars.om == nil {
|
||||
return 0
|
||||
}
|
||||
defer vars.mutex.RUnlock()
|
||||
vars.mutex.RLock()
|
||||
return vars.om.Len()
|
||||
}
|
||||
|
||||
// Get returns the value the the variable with the provided key and a boolean
|
||||
// that indicates if the value was found or not. If the value is not found, the
|
||||
// returned variable is a zero value and the bool is false.
|
||||
func (vars *Vars) Get(key string) (Var, bool) {
|
||||
if vars == nil || vars.om == nil {
|
||||
return Var{}, false
|
||||
}
|
||||
defer vars.mutex.RUnlock()
|
||||
vars.mutex.RLock()
|
||||
return vars.om.Get(key)
|
||||
}
|
||||
|
||||
// Set sets the value of the variable with the provided key to the provided
|
||||
// value. If the variable already exists, its value is updated. If the variable
|
||||
// does not exist, it is created.
|
||||
func (vars *Vars) Set(key string, value Var) bool {
|
||||
if vars == nil {
|
||||
vars = NewVars()
|
||||
}
|
||||
if vars.om == nil {
|
||||
vars.om = orderedmap.NewOrderedMap[string, Var]()
|
||||
}
|
||||
defer vars.mutex.Unlock()
|
||||
vars.mutex.Lock()
|
||||
return vars.om.Set(key, value)
|
||||
}
|
||||
|
||||
// All returns an iterator that loops over all task key-value pairs.
|
||||
func (vars *Vars) All() iter.Seq2[string, Var] {
|
||||
if vars == nil || vars.om == nil {
|
||||
return func(yield func(string, Var) bool) {}
|
||||
}
|
||||
return vars.om.AllFromFront()
|
||||
}
|
||||
|
||||
// Keys returns an iterator that loops over all task keys.
|
||||
func (vars *Vars) Keys() iter.Seq[string] {
|
||||
if vars == nil || vars.om == nil {
|
||||
return func(yield func(string) bool) {}
|
||||
}
|
||||
return vars.om.Keys()
|
||||
}
|
||||
|
||||
// Values returns an iterator that loops over all task values.
|
||||
func (vars *Vars) Values() iter.Seq[Var] {
|
||||
if vars == nil || vars.om == nil {
|
||||
return func(yield func(Var) bool) {}
|
||||
}
|
||||
return vars.om.Values()
|
||||
}
|
||||
|
||||
// ToCacheMap converts Vars to an unordered map containing only the static
|
||||
// variables
|
||||
func (vars *Vars) ToCacheMap() (m map[string]any) {
|
||||
defer vars.mutex.RUnlock()
|
||||
vars.mutex.RLock()
|
||||
m = make(map[string]any, vars.Len())
|
||||
for k, v := range vars.All() {
|
||||
if v.Sh != nil && *v.Sh != "" {
|
||||
// Dynamic variable is not yet resolved; trigger
|
||||
// <no value> to be used in templates.
|
||||
return nil
|
||||
}
|
||||
if v.Live != nil {
|
||||
m[k] = v.Live
|
||||
} else {
|
||||
m[k] = v.Value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Merge loops over other and merges it values with the variables in vars. If
|
||||
// the include parameter is not nil and its it is an advanced import, the
|
||||
// directory is set set to the value of the include parameter.
|
||||
func (vars *Vars) Merge(other *Vars, include *Include) {
|
||||
if vars == nil || vars.om == nil || other == nil {
|
||||
return
|
||||
}
|
||||
defer other.mutex.RUnlock()
|
||||
other.mutex.RLock()
|
||||
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
|
||||
if include != nil && include.AdvancedImport {
|
||||
pair.Value.Dir = include.Dir
|
||||
}
|
||||
vars.om.Set(pair.Key, pair.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (vs *Vars) DeepCopy() *Vars {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
defer vs.mutex.RUnlock()
|
||||
vs.mutex.RLock()
|
||||
return &Vars{
|
||||
om: deepcopy.OrderedMap(vs.om),
|
||||
}
|
||||
}
|
||||
|
||||
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
|
||||
if vs == nil || vs.om == nil {
|
||||
*vs = *NewVars()
|
||||
}
|
||||
vs.om = orderedmap.NewOrderedMap[string, Var]()
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
|
||||
// the map manually. We increment over 2 values at a time and assign
|
||||
// them as a key-value pair.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
// Decode the value node into a Task struct
|
||||
var v Var
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
// Add the task to the ordered map
|
||||
vs.Set(keyNode.Value, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
@@ -21,7 +22,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := &ast.Vars{}
|
||||
env := ast.NewVars()
|
||||
cache := &templater.Cache{Vars: vars}
|
||||
|
||||
for _, dotEnvPath := range tf.Dotenv {
|
||||
@@ -37,10 +38,10 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
|
||||
|
||||
envs, err := godotenv.Read(dotEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, err)
|
||||
}
|
||||
for key, value := range envs {
|
||||
if ok := env.Exists(key); !ok {
|
||||
if _, ok := env.Get(key); !ok {
|
||||
env.Set(key, ast.Var{Value: value})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
giturls "github.com/chainguard-dev/git-urls"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
@@ -24,7 +25,6 @@ type Node interface {
|
||||
}
|
||||
|
||||
func NewRootNode(
|
||||
l *logger.Logger,
|
||||
entrypoint string,
|
||||
dir string,
|
||||
insecure bool,
|
||||
@@ -35,11 +35,10 @@ func NewRootNode(
|
||||
if entrypoint == "-" {
|
||||
return NewStdinNode(dir)
|
||||
}
|
||||
return NewNode(l, entrypoint, dir, insecure, timeout)
|
||||
return NewNode(entrypoint, dir, insecure, timeout)
|
||||
}
|
||||
|
||||
func NewNode(
|
||||
l *logger.Logger,
|
||||
entrypoint string,
|
||||
dir string,
|
||||
insecure bool,
|
||||
@@ -48,24 +47,39 @@ func NewNode(
|
||||
) (Node, error) {
|
||||
var node Node
|
||||
var err error
|
||||
switch getScheme(entrypoint) {
|
||||
case "http", "https":
|
||||
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
|
||||
default:
|
||||
// If no other scheme matches, we assume it's a file
|
||||
node, err = NewFileNode(l, entrypoint, dir, opts...)
|
||||
scheme, err := getScheme(entrypoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
|
||||
switch scheme {
|
||||
case "git":
|
||||
node, err = NewGitNode(entrypoint, dir, insecure, opts...)
|
||||
case "http", "https":
|
||||
node, err = NewHTTPNode(entrypoint, dir, insecure, timeout, opts...)
|
||||
default:
|
||||
node, err = NewFileNode(entrypoint, dir, opts...)
|
||||
|
||||
}
|
||||
|
||||
if node.Remote() && !experiments.RemoteTaskfiles.Enabled() {
|
||||
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
|
||||
}
|
||||
return node, err
|
||||
}
|
||||
|
||||
func getScheme(uri string) string {
|
||||
if i := strings.Index(uri, "://"); i != -1 {
|
||||
return uri[:i]
|
||||
func getScheme(uri string) (string, error) {
|
||||
u, err := giturls.Parse(uri)
|
||||
if u == nil {
|
||||
return "", err
|
||||
}
|
||||
return ""
|
||||
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
|
||||
return "git", nil
|
||||
}
|
||||
|
||||
if i := strings.Index(uri, "://"); i != -1 {
|
||||
return uri[:i], nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func getDefaultDir(entrypoint, dir string) string {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
)
|
||||
|
||||
// A FileNode is a node that reads a taskfile from the local filesystem.
|
||||
@@ -18,10 +17,10 @@ type FileNode struct {
|
||||
Entrypoint string
|
||||
}
|
||||
|
||||
func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
|
||||
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
|
||||
var err error
|
||||
base := NewBaseNode(dir, opts...)
|
||||
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, base.dir)
|
||||
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(entrypoint, base.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,10 +49,10 @@ func (node *FileNode) Read(ctx context.Context) ([]byte, error) {
|
||||
|
||||
// resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and
|
||||
// populates them with default values if necessary.
|
||||
func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (string, string, error) {
|
||||
func resolveFileNodeEntrypointAndDir(entrypoint, dir string) (string, string, error) {
|
||||
var err error
|
||||
if entrypoint != "" {
|
||||
entrypoint, err = Exists(l, entrypoint)
|
||||
entrypoint, err = Exists(entrypoint)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -68,7 +67,7 @@ func resolveFileNodeEntrypointAndDir(l *logger.Logger, entrypoint, dir string) (
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
entrypoint, err = ExistsWalk(l, dir)
|
||||
entrypoint, err = ExistsWalk(dir)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -81,6 +80,9 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||
if strings.Contains(entrypoint, "://") {
|
||||
return entrypoint, nil
|
||||
}
|
||||
if strings.HasPrefix(entrypoint, "git") {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
path, err := execext.Expand(entrypoint)
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user