Compare commits

..

2 Commits

Author SHA1 Message Date
Valentin Maerten
e639dfae32 refactor: VeryFastCompile for Task list 2025-02-09 20:01:35 +01:00
Pete Davison
bc85be2c47 chore: changelog for #2049 2025-02-09 19:52:15 +01:00
363 changed files with 9194 additions and 14133 deletions

46
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

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

14
.github/CONTRIBUTING.md vendored Normal file
View File

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

3
.github/FUNDING.yml vendored Normal file
View File

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

View File

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

View File

@@ -13,7 +13,7 @@ jobs:
name: Lint name: Lint
strategy: strategy:
matrix: matrix:
go-version: [1.23.x, 1.24.x] go-version: [1.22.x, 1.23.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
@@ -23,9 +23,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v6
with: with:
version: v2.1.0 version: v1.60.1
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Get changed files in the docs folder - name: Get changed files in the docs folder
id: changed-files-specific id: changed-files-specific
uses: tj-actions/changed-files@v46 uses: tj-actions/changed-files@v45
with: with:
files: website/versioned_docs/** files: website/versioned_docs/**
@@ -56,19 +56,3 @@ jobs:
with: with:
script: | script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.') core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')
check_schema:
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@v46
with:
files: |
website/static/schema.json
website/static/schema-taskrc.json
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('schema.json or schema-taskrc.json has changed. Instead you need to update next-schema.json or next-schema-taskrc.json.')

View File

@@ -1,29 +0,0 @@
name: Realease nightly
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * *
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean --nightly -f .goreleaser-nightly.yml
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
pro: true
release:
name_template: 'v{{.Version}}'
nightly:
publish_release: true
keep_single_release: true
version_template: "{{incminor .Version}}-nightly"
includes:
- from_file:
path: ./.goreleaser.yml

View File

@@ -30,45 +30,42 @@ builds:
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
- "-s -w" - -s -w # Don't set main.version.
- "{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}"
gomod: gomod:
proxy: true proxy: true
archives: archives:
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}' - name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
files: files:
- README.md - README.md
- LICENSE - LICENSE
- completion/**/* - completion/**/*
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: [zip] format: zip
git: release:
ignore_tags: draft: true
- "{{if not .IsNightly}}nightly{{end}}"
snapshot: snapshot:
version_template: '{{.Version}}' version_template: "{{.Version}}"
checksum: checksum:
name_template: 'task_checksums.txt' name_template: "task_checksums.txt"
nfpms: nfpms:
- vendor: Task - vendor: Task
homepage: https://taskfile.dev homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev> maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go description: Simple task runner written in Go
section: golang
license: MIT license: MIT
conflicts: conflicts:
- taskwarrior - taskwarrior
formats: formats:
- deb - deb
- rpm - rpm
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}' file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
contents: contents:
- src: completion/bash/task.bash - src: completion/bash/task.bash
dst: /etc/bash_completion.d/task dst: /etc/bash_completion.d/task
@@ -86,7 +83,8 @@ brews:
repository: repository:
owner: go-task owner: go-task
name: homebrew-tap name: homebrew-tap
test: system "#{bin}/task", "--help" test:
system "#{bin}/task", "--help"
install: |- install: |-
bin.install "task" bin.install "task"
bash_completion.install "completion/bash/task.bash" => "task" bash_completion.install "completion/bash/task.bash" => "task"
@@ -109,7 +107,7 @@ winget:
commit_author: commit_author:
name: task-bot name: task-bot
email: 106601941+task-bot@users.noreply.github.com email: 106601941+task-bot@users.noreply.github.com
commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}' commit_msg_template: "chore: bump {{.PackageIdentifier}} to {{.Tag}}"
release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}} release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}
tags: tags:
- build - build
@@ -123,15 +121,13 @@ winget:
- task-runner - task-runner
- taskfile - taskfile
- tool - tool
skip_upload: true
repository: repository:
owner: go-task owner: microsoft
name: winget-pkgs name: winget-pkgs
branch: 'chore/task-{{.Version}}'
pull_request: pull_request:
enabled: true enabled: true
draft: false
check_boxes: true
base: base:
owner: microsoft owner: go-task
name: winget-pkgs name: winget-pkgs
branch: master branch: "bump-task-to-{{.Tag}}"

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
22.18.0 22.13.1

View File

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

View File

@@ -1,138 +1,9 @@
# Changelog # Changelog
## v3.44.1 - 2025-07-23 ## Unreleased
- Internal tasks will no longer be shown as suggestions since they cannot be
called (#2309, #2323 by @maxmzkrcensys)
- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).
- Fixed a regression where fingerprinting was not working correctly if the path
to you Taskfile contained a space (#2321, #2322 by @pd93).
- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).
- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests
(#2265 by @pd93).
## v3.44.0 - 2025-06-08
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
@pd93).
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
string). (#2138, #2139, #2140 by @pd93).
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
- Added the ability to
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
by specifying a checksum. This works with both local and remote Taskfiles
(#2222, #2223 by @pd93).
- When using the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
any credentials used in the URL will now be redacted in Task's output (#2100,
#2220 by @pd93).
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
by @vmaerten).
- Fixed a bug where taskfiles in directories containing spaces created
directories in the wrong location (#2208, #2216 by @pd93).
- Added support for dual JSON schema files, allowing changes without affecting
the current schema. The current schemas will only be updated during releases.
(#2211 by @vmaerten).
- Improved fingerprint documentation by specifying that the method can be set at
the root level to apply to all tasks (#2233 by @vmaerten).
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
@wazazaby, #2271 by @andreynering).
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by
@vmaerten).
## v3.43.1 - 2025-04-21
- Significant improvements were made to the watcher. We migrated from
[watcher](https://github.com/radovskyb/watcher) to
[fsnotify](https://github.com/fsnotify/fsnotify). The former library used
polling, which means Task had a high CPU usage when watching too many files.
`fsnotify` uses proper the APIs from each operating system to watch files,
which means a much better performance. The default interval changed from 5
seconds to 100 milliseconds, because now it configures the wait time for
duplicated events, instead of the polling time (#2048 by @andreynering, #1508,
#985, #1179).
- The [Map Variables experiment](https://github.com/go-task/task/issues/1585)
was made generally available so you can now
[define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables)
(#1585, #1547, #2081 by @pd93).
- Wildcards can now
[match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072,
#2121 by @pd93).
- Added the ability to
[loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files).
This works the same way as looping over sources (#2151 by @sedyh).
- Added the ability to resolve variables when defining an include variable
(#2108, #2113 by @pd93).
- A few changes have been made to the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317)
(#1402, #2176 by @pd93):
- Cached files are now prioritized over remote ones.
- Added an `--expiry` flag which sets the TTL for a remote file cache. By
default the value will be 0 (caching disabled). If Task is running in
offline mode or fails to make a connection, it will fallback on the cache.
- `.taskrc` files can now be used from subdirectories and will be searched for
recursively up the file tree in the same way that Taskfiles are (#2159, #2166
by @pd93).
- The default taskfile (output when using the `--init` flag) is now an embedded
file in the binary instead of being stored in the code (#2112 by @pd93).
- Improved the way we report the Task version when using the `--version` flag or
`{{.TASK_VERSION}}` variable. This should now be more consistent and easier
for package maintainers to use (#2131 by @pd93).
- Fixed a bug where globstar (`**`) matching in `sources` only resolved the
first result (#2073, #2075 by @pd93).
- Fixed a bug where sorting tasks by "none" would use the default sorting
instead of leaving tasks in the order they were defined (#2124, #2125 by
@trulede).
- Fixed Fish completion on newer Fish versions (#2130 by @atusy).
- Fixed a bug where undefined/null variables resolved to an empty string instead
of `nil` (#1911, #2144 by @pd93).
- The `USER_WORKING_DIR` special now will now properly account for the `--dir`
(`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering).
- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).
- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).
#### Package API
- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now
uses the functional options pattern (#2085, #2147, #2148 by @pd93).
- The functional options for the
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
and
[`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by
@pd93).
- [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
no longer accepts a
[`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node).
Instead nodes are passed directly into the
[`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
method (#2169 by @pd93).
- [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)
also now accepts a [`context.Context`](https://pkg.go.dev/context#Context)
(#2176 by @pd93).
## v3.42.1 - 2025-03-10
- Fixed a bug where some special variables caused a type error when used global
variables (#2106, #2107 by @pd93).
## v3.42.0 - 2025-03-08
- Made `--init` less verbose by default and respect `--silent` and `--verbose` - Made `--init` less verbose by default and respect `--silent` and `--verbose`
flags (#2009, #2011 by @HeCorr). 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 - Fix a bug where an HTTP node's location was being mutated incorrectly (#2007
by @jeongukjae). by @jeongukjae).
- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by - Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by
@@ -142,73 +13,6 @@ Reverted the changes made in #2113 and #2186 that affected the
- Print warnings when attempting to enable an inactive experiment or an active - Print warnings when attempting to enable an inactive experiment or an active
experiment with an invalid value (#1979, #2049 by @pd93). experiment with an invalid value (#1979, #2049 by @pd93).
- Refactored the experiments package and added tests (#2049 by @pd93). - Refactored the experiments package and added tests (#2049 by @pd93).
- Show allowed values when a variable with an enum is missing (#2027, #2052 by
@vmaerten).
- Refactored how snippets in error work and added tests (#2068 by @pd93).
- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by
@pd93).
- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand
`cmds` syntax were not considered valid (#2068 by @pd93).
- Refactored how task sorting functions work (#1798 by @pd93).
- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable
experiments (similar to `.env`) (#1982 by @vmaerten).
- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086
by @pd93).
- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).
- Fixed a bug where, when a dynamic variable is provided, even if it is not
used, all other variables become unavailable in the templating system within
the include (#2092 by @vmaerten).
#### Package API
Unlike our CLI tool,
[Task's package API is not currently stable](https://taskfile.dev/reference/package).
In an effort to ease the pain of breaking changes for our users, we will be
providing changelogs for our package API going forwards. The hope is that these
changes will provide a better long-term experience for our users and allow to
stabilize the API in the future. #121 now tracks this piece of work.
- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).
- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)
(#2011, ff8c913 by @HeCorr and @pd93)
- No longer accepts an `io.Writer` (output is now the caller's
responsibility).
- The path argument can now be a filename OR a directory.
- The function now returns the full path of the generated file.
- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)
now accepts a string instead of the arguments required to generate a snippet
(#2068 by @pd93).
- The caller is now expected to create the snippet themselves (see below).
- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)
and related code moved from the `errors` package to the `taskfile` package
(#2068 by @pd93).
- Renamed `TaskMissingRequiredVars` to
[`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)
(#2052 by @vmaerten).
- Renamed `TaskNotAllowedVars` to
[`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)
(#2052 by @vmaerten).
- The
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
is now constructed using the functional options pattern (#2082 by @pd93).
- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082
by @pd93).
- Users are now expected to pass a custom debug/prompt functions into
[`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)
if they want this functionality by using the new
[`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)
and
[`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)
functional options.
- Remove `Range` functions in the `taskfile/ast` package in favour of new
iterator functions (#1798 by @pd93).
- `ast.Call` was moved from the `taskfile/ast` package to the main `task`
package (#2084 by @pd93).
- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the
`task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).
- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were
moved from the `internal/compiler` package to the main `task` package (#2084
by @pd93).
## v3.41.0 - 2025-01-18 ## v3.41.0 - 2025-01-18

View File

@@ -18,11 +18,6 @@ tasks:
- task: lint - task: lint
- task: test - task: test
run:
desc: Runs Task
cmds:
- go run ./cmd/task {{.CLI_ARGS}}
install: install:
desc: Installs Task desc: Installs Task
aliases: [i] aliases: [i]
@@ -32,44 +27,27 @@ tasks:
- go install -v ./cmd/task - go install -v ./cmd/task
generate: generate:
aliases: [gen, g]
desc: Runs all generate tasks
cmds:
- task: generate:mocks
- task: generate:fixtures
generate:mocks:
desc: Runs Mockery to create mocks desc: Runs Mockery to create mocks
aliases: [gen:mocks, g:mocks] aliases: [gen, g]
deps: [install:mockery] deps: [install:mockery]
sources: sources:
- "internal/fingerprint/checker.go" - "internal/fingerprint/checker.go"
generates: generates:
- "internal/mocks/*.go" - "internal/mocks/*.go"
cmds: cmds:
- find . -type f -name *_mock.go -delete - "{{.BIN}}/mockery --dir ./internal/fingerprint --name SourcesCheckable"
- "{{.BIN}}/mockery" - "{{.BIN}}/mockery --dir ./internal/fingerprint --name StatusCheckable"
generate:fixtures:
desc: Runs tests and generates golden fixture files
aliases: [gen:fixtures, g:fixtures]
env:
GOLDIE_UPDATE: 'true'
GOLDIE_TEMPLATE: 'true'
cmds:
- find ./testdata -name '*.golden' -delete
- go test ./...
install:mockery: install:mockery:
desc: Installs mockgen; a tool to generate mock files desc: Installs mockgen; a tool to generate mock files
vars: vars:
MOCKERY_VERSION: v3.2.2 MOCKERY_VERSION: v2.24.0
env: env:
GOBIN: "{{.BIN}}" GOBIN: "{{.BIN}}"
status: status:
- go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}} - go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}}
cmds: cmds:
- GOBIN="{{.BIN}}" go install github.com/vektra/mockery/v3@{{.MOCKERY_VERSION}} - go install github.com/vektra/mockery/v2@{{.MOCKERY_VERSION}}
mod: mod:
desc: Downloads and tidy Go modules desc: Downloads and tidy Go modules
@@ -90,7 +68,6 @@ tasks:
sources: sources:
- './**/*.go' - './**/*.go'
- .golangci.yml - .golangci.yml
- go.mod
cmds: cmds:
- golangci-lint run - golangci-lint run
@@ -99,19 +76,9 @@ tasks:
sources: sources:
- './**/*.go' - './**/*.go'
- .golangci.yml - .golangci.yml
- go.mod
cmds: cmds:
- golangci-lint run --fix - golangci-lint run --fix
format:
desc: Runs golangci-lint and formats any Go files
aliases: [fmt, f]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint fmt
sleepit:build: sleepit:build:
desc: Builds the sleepit test helper desc: Builds the sleepit test helper
sources: sources:
@@ -137,12 +104,6 @@ tasks:
cmds: cmds:
- go test ./... - go test ./...
test:watch:
desc: Runs test suite with watch tests included
deps: [sleepit:build]
cmds:
- go test ./... -tags 'watch'
test:all: test:all:
desc: Runs test suite with signals and watch tests included desc: Runs test suite with signals and watch tests included
deps: [sleepit:build] deps: [sleepit:build]
@@ -159,22 +120,6 @@ tasks:
cmds: cmds:
- go install github.com/goreleaser/goreleaser/v2@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:*: release:*:
desc: Prepare the project for a new release desc: Prepare the project for a new release
summary: | summary: |
@@ -188,7 +133,7 @@ tasks:
- Push the commit/tag to the repository - Push the commit/tag to the repository
- Create a GitHub release - Create a GitHub release
To use the task, run "task release:<version>" where "<version>" is is one of: To use the task, simply run "task release:<version>" where "<version>" is is one of:
- "major" - Bumps the major number - "major" - Bumps the major number
- "minor" - Bumps the minor number - "minor" - Bumps the minor number

View File

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

View File

@@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args" "github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -16,12 +15,12 @@ func TestArgs(t *testing.T) {
tests := []struct { tests := []struct {
Args []string Args []string
ExpectedCalls []*task.Call ExpectedCalls []*ast.Call
ExpectedGlobals *ast.Vars ExpectedGlobals *ast.Vars
}{ }{
{ {
Args: []string{"task-a", "task-b", "task-c"}, Args: []string{"task-a", "task-b", "task-c"},
ExpectedCalls: []*task.Call{ ExpectedCalls: []*ast.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
@@ -29,7 +28,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
ExpectedCalls: []*task.Call{ ExpectedCalls: []*ast.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
@@ -57,7 +56,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"task-a", "CONTENT=with some spaces"}, Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []*task.Call{ ExpectedCalls: []*ast.Call{
{Task: "task-a"}, {Task: "task-a"},
}, },
ExpectedGlobals: ast.NewVars( ExpectedGlobals: ast.NewVars(
@@ -71,7 +70,7 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: []string{"FOO=bar", "task-a", "task-b"}, Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []*task.Call{ ExpectedCalls: []*ast.Call{
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
}, },
@@ -86,15 +85,15 @@ func TestArgs(t *testing.T) {
}, },
{ {
Args: nil, Args: nil,
ExpectedCalls: []*task.Call{}, ExpectedCalls: []*ast.Call{},
}, },
{ {
Args: []string{}, Args: []string{},
ExpectedCalls: []*task.Call{}, ExpectedCalls: []*ast.Call{},
}, },
{ {
Args: []string{"FOO=bar", "BAR=baz"}, Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []*task.Call{}, ExpectedCalls: []*ast.Call{},
ExpectedGlobals: ast.NewVars( ExpectedGlobals: ast.NewVars(
&ast.VarElement{ &ast.VarElement{
Key: "FOO", Key: "FOO",

View File

@@ -16,14 +16,10 @@ import (
) )
const ( const (
changelogSource = "CHANGELOG.md" changelogSource = "CHANGELOG.md"
changelogTarget = "website/docs/changelog.mdx" changelogTarget = "website/docs/changelog.mdx"
docsSource = "website/docs" docsSource = "website/docs"
docsTarget = "website/versioned_docs/version-latest" docsTarget = "website/versioned_docs/version-latest"
schemaSource = "website/static/next-schema.json"
schemaTarget = "website/static/schema.json"
schemaTaskrcSource = "website/static/next-schema-taskrc.json"
schemaTaskrcTarget = "website/static/schema-taskrc.json"
) )
var ( var (
@@ -71,10 +67,6 @@ func release() error {
return err return err
} }
if err := setVersionFile("internal/version/version.txt", version); err != nil {
return err
}
if err := setJSONVersion("package.json", version); err != nil { if err := setJSONVersion("package.json", version); err != nil {
return err return err
} }
@@ -87,10 +79,6 @@ func release() error {
return err return err
} }
if err := schema(); err != nil {
return err
}
return nil return nil
} }
@@ -156,10 +144,6 @@ func changelog(version *semver.Version) error {
return os.WriteFile(changelogTarget, []byte(changelog), 0o644) return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
} }
func setVersionFile(fileName string, version *semver.Version) error {
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644)
}
func setJSONVersion(fileName string, version *semver.Version) error { func setJSONVersion(fileName string, version *semver.Version) error {
// Read the JSON file // Read the JSON file
b, err := os.ReadFile(fileName) b, err := os.ReadFile(fileName)
@@ -183,13 +167,3 @@ func docs() error {
} }
return nil return nil
} }
func schema() error {
if err := copy.Copy(schemaSource, schemaTarget); err != nil {
return err
}
if err := copy.Copy(schemaTaskrcSource, schemaTaskrcTarget); err != nil {
return err
}
return nil
}

View File

@@ -4,18 +4,19 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath" "strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"mvdan.cc/sh/v3/syntax"
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/args" "github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments" "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/flags"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -54,12 +55,11 @@ func run() error {
return err return err
} }
if err := experiments.Validate(); err != nil { dir := flags.Dir
log.Warnf("%s\n", err.Error()) entrypoint := flags.Entrypoint
}
if flags.Version { if flags.Version {
fmt.Println(version.GetVersionWithBuildInfo()) fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil return nil
} }
@@ -77,28 +77,18 @@ func run() error {
if err != nil { if err != nil {
return err return err
} }
args, _, err := args.Get()
if err != nil { if err := task.InitTaskfile(os.Stdout, wd); 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 return err
} }
if !flags.Silent { if !flags.Silent {
if flags.Verbose { if flags.Verbose {
log.Outf(logger.Default, "%s\n", task.DefaultTaskfile) log.Outf(logger.Default, "%s\n", task.DefaultTaskfile)
} }
log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath)) log.Outf(logger.Green, "%s created in the current directory\n", task.DefaultTaskFilename)
} }
return nil return nil
} }
@@ -111,51 +101,60 @@ func run() error {
return nil return nil
} }
if flags.Global {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("task: Failed to get user home directory: %w", err)
}
dir = home
}
if err := experiments.Validate(); err != nil { if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error()) log.Warnf("%s\n", err.Error())
} }
// Create a new root node for the given entrypoint var taskSorter sort.TaskSorter
node, err := taskfile.NewRootNode( switch flags.TaskSort {
flags.Entrypoint, case "none":
flags.Dir, taskSorter = &sort.Noop{}
flags.Insecure, case "alphanumeric":
) taskSorter = &sort.AlphaNumeric{}
if err != nil { }
e := task.Executor{
Dir: dir,
Entrypoint: entrypoint,
Force: flags.Force,
ForceAll: flags.ForceAll,
Insecure: flags.Insecure,
Download: flags.Download,
Offline: flags.Offline,
Timeout: flags.Timeout,
Watch: flags.Watch,
Verbose: flags.Verbose,
Silent: flags.Silent,
AssumeYes: flags.AssumeYes,
Dry: flags.Dry || flags.Status,
Summary: flags.Summary,
Parallel: flags.Parallel,
Color: flags.Color,
Concurrency: flags.Concurrency,
Interval: flags.Interval,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
OutputStyle: flags.Output,
TaskSorter: taskSorter,
EnableVersionCheck: true,
}
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil {
return err return err
} }
tempDir, err := task.NewTempDir(node.Dir()) err := e.Setup()
if err != nil {
return err
}
reader := taskfile.NewReader(
flags.WithFlags(),
taskfile.WithTempDir(tempDir.Remote),
taskfile.WithDebugFunc(func(s string) {
log.VerboseOutf(logger.Magenta, s)
}),
taskfile.WithPromptFunc(func(s string) error {
return log.Prompt(logger.Yellow, s, "n", "y", "yes")
}),
)
ctx, cf := context.WithTimeout(context.Background(), flags.Timeout)
defer cf()
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: flags.Timeout}
}
return err
}
executor, err := task.NewExecutor(graph,
flags.WithFlags(),
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
)
if err != nil { if err != nil {
return err return err
} }
@@ -167,21 +166,19 @@ func run() error {
} }
if flags.ClearCache { if flags.ClearCache {
cachePath := filepath.Join(executor.TempDir.Remote, "remote") cache, err := taskfile.NewCache(e.TempDir.Remote)
return os.RemoveAll(cachePath) if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll)
} }
listOptions := task.NewListOptions(
flags.List,
flags.ListAll,
flags.ListJson,
flags.NoStatus,
)
if listOptions.ShouldListTasks() { if listOptions.ShouldListTasks() {
if flags.Silent { foundTasks, err := e.ListTasks(listOptions)
return executor.ListTaskNames(flags.ListAll)
}
foundTasks, err := executor.ListTasks(listOptions)
if err != nil { if err != nil {
return err return err
} }
@@ -191,39 +188,60 @@ func run() error {
return nil return nil
} }
// Parse the remaining arguments var (
cliArgsPreDash, cliArgsPostDash, err := args.Get() calls []*ast.Call
globals *ast.Vars
)
tasksAndVars, cliArgs, err := getArgs()
if err != nil { if err != nil {
return err return err
} }
calls, globals := args.Parse(cliArgsPreDash...)
calls, globals = args.Parse(tasksAndVars...)
// If there are no calls, run the default task instead // If there are no calls, run the default task instead
if len(calls) == 0 { if len(calls) == 0 {
calls = append(calls, &task.Call{Task: "default"}) calls = append(calls, &ast.Call{Task: "default"})
} }
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash) globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
if err != nil {
return err
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline}) globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
executor.Taskfile.Vars.Merge(globals, nil) e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch { if !flags.Watch {
executor.InterceptInterruptSignals() e.InterceptInterruptSignals()
} }
ctx = context.Background() ctx := context.Background()
if flags.Status { if flags.Status {
return executor.Status(ctx, calls...) return e.Status(ctx, calls...)
} }
return executor.Run(ctx, calls...) return e.Run(ctx, calls...)
}
func getArgs() ([]string, string, error) {
var (
args = pflag.Args()
doubleDashPos = pflag.CommandLine.ArgsLenAtDash()
)
if doubleDashPos == -1 {
return args, "", nil
}
var quotedCliArgs []string
for _, arg := range args[doubleDashPos:] {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return nil, "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
} }

View File

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

View File

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

View File

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

View File

@@ -8,11 +8,6 @@ const (
CodeUnknown // Used when no other exit code is appropriate CodeUnknown // Used when no other exit code is appropriate
) )
// TaskRC related exit codes
const (
CodeTaskRCNotFoundError int = iota + 50
)
// Taskfile related exit codes // Taskfile related exit codes
const ( const (
CodeTaskfileNotFound int = iota + 100 CodeTaskfileNotFound int = iota + 100
@@ -26,7 +21,6 @@ const (
CodeTaskfileNetworkTimeout CodeTaskfileNetworkTimeout
CodeTaskfileInvalid CodeTaskfileInvalid
CodeTaskfileCycle CodeTaskfileCycle
CodeTaskfileDoesNotMatchChecksum
) )
// Task related exit codes // Task related exit codes

View File

@@ -1,7 +1,6 @@
package errors package errors
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -47,9 +46,8 @@ func (err *TaskRunError) Code() int {
} }
func (err *TaskRunError) TaskExitCode() int { func (err *TaskRunError) TaskExitCode() int {
var exit interp.ExitStatus if c, ok := interp.IsExitStatus(err.Err); ok {
if errors.As(err.Err, &exit) { return int(c)
return int(exit)
} }
return err.Code() return err.Code()
} }
@@ -143,37 +141,21 @@ func (err *TaskCancelledNoTerminalError) Code() int {
return CodeTaskCancelled return CodeTaskCancelled
} }
// TaskMissingRequiredVarsError is returned when a task is missing required variables. // TaskMissingRequiredVars is returned when a task is missing required variables.
type TaskMissingRequiredVars struct {
type MissingVar struct {
Name string
AllowedValues []string
}
type TaskMissingRequiredVarsError struct {
TaskName string TaskName string
MissingVars []MissingVar MissingVars []string
} }
func (v MissingVar) String() string { func (err *TaskMissingRequiredVars) Error() 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( return fmt.Sprintf(
`task: Task %q cancelled because it is missing required variables: %s`, `task: Task %q cancelled because it is missing required variables: %s`,
err.TaskName, err.TaskName,
strings.Join(vars, ", ")) strings.Join(err.MissingVars, ", "),
)
} }
func (err *TaskMissingRequiredVarsError) Code() int { func (err *TaskMissingRequiredVars) Code() int {
return CodeTaskMissingRequiredVars return CodeTaskMissingRequiredVars
} }
@@ -183,12 +165,12 @@ type NotAllowedVar struct {
Name string Name string
} }
type TaskNotAllowedVarsError struct { type TaskNotAllowedVars struct {
TaskName string TaskName string
NotAllowedVars []NotAllowedVar NotAllowedVars []NotAllowedVar
} }
func (err *TaskNotAllowedVarsError) Error() string { func (err *TaskNotAllowedVars) Error() string {
var builder strings.Builder var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
@@ -199,6 +181,6 @@ func (err *TaskNotAllowedVarsError) Error() string {
return builder.String() return builder.String()
} }
func (err *TaskNotAllowedVarsError) Code() int { func (err *TaskNotAllowedVars) Code() int {
return CodeTaskNotAllowedVars return CodeTaskNotAllowedVars
} }

View File

@@ -155,14 +155,19 @@ func (err *TaskfileVersionCheckError) Code() int {
// TaskfileNetworkTimeoutError is returned when the user attempts to use a remote // TaskfileNetworkTimeoutError is returned when the user attempts to use a remote
// Taskfile but a network connection could not be established within the timeout. // Taskfile but a network connection could not be established within the timeout.
type TaskfileNetworkTimeoutError struct { type TaskfileNetworkTimeoutError struct {
URI string URI string
Timeout time.Duration Timeout time.Duration
CheckedCache bool
} }
func (err *TaskfileNetworkTimeoutError) Error() string { func (err *TaskfileNetworkTimeoutError) Error() string {
var cacheText string
if err.CheckedCache {
cacheText = " and no offline copy was found in the cache"
}
return fmt.Sprintf( return fmt.Sprintf(
`task: Network connection timed out after %s while attempting to download Taskfile %q`, `task: Network connection timed out after %s while attempting to download Taskfile %q%s`,
err.Timeout, err.URI, err.Timeout, err.URI, cacheText,
) )
} }
@@ -187,24 +192,3 @@ func (err TaskfileCycleError) Error() string {
func (err TaskfileCycleError) Code() int { func (err TaskfileCycleError) Code() int {
return CodeTaskfileCycle return CodeTaskfileCycle
} }
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
// match the one pinned in the parent Taskfile.
type TaskfileDoesNotMatchChecksum struct {
URI string
ExpectedChecksum string
ActualChecksum string
}
func (err *TaskfileDoesNotMatchChecksum) Error() string {
return fmt.Sprintf(
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
err.URI,
err.ActualChecksum,
err.ExpectedChecksum,
)
}
func (err *TaskfileDoesNotMatchChecksum) Code() int {
return CodeTaskfileDoesNotMatchChecksum
}

View File

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

View File

@@ -1,422 +0,0 @@
package task
import (
"context"
"io"
"os"
"path/filepath"
"sync"
"time"
"github.com/puzpuzpuz/xsync/v3"
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
)
type (
// An ExecutorOption is any type that can apply a configuration to an
// [Executor].
ExecutorOption interface {
ApplyToExecutor(*Executor)
}
// An Executor is used for processing Taskfile(s) and executing the task(s)
// within them.
Executor struct {
// Flags
Dir string
TempDir *TempDir
Force bool
ForceAll bool
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
// I/O
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Internal
Taskfile *ast.Taskfile
Logger *logger.Logger
Compiler *Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.Sorter
UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool]
}
)
// NewExecutor creates a new [Executor] and applies the given functional options
// to it.
func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, error) {
tf, err := graph.Merge()
if err != nil {
return nil, err
}
e := &Executor{
Taskfile: tf,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Logger: nil,
Compiler: nil,
Output: nil,
OutputStyle: ast.Output{},
TaskSorter: sort.AlphaNumericWithRootTasksFirst,
UserWorkingDir: "",
fuzzyModel: nil,
concurrencySemaphore: nil,
taskCallCount: map[string]*int32{},
mkdirMutexMap: map[string]*sync.Mutex{},
executionHashes: map[string]context.Context{},
executionHashesMutex: sync.Mutex{},
}
e.Options(opts...)
if err := e.setup(); err != nil {
return nil, err
}
return e, nil
}
// Options loops through the given [ExecutorOption] functions and applies them
// to the [Executor].
func (e *Executor) Options(opts ...ExecutorOption) {
for _, opt := range opts {
opt.ApplyToExecutor(e)
}
}
// WithDir sets the working directory of the [Executor]. By default, the
// directory is set to the user's current working directory.
func WithDir(dir string) ExecutorOption {
return &dirOption{dir}
}
type dirOption struct {
dir string
}
func (o *dirOption) ApplyToExecutor(e *Executor) {
absDir, err := filepath.Abs(o.dir)
if err != nil {
e.Dir = o.dir
return
}
e.Dir = absDir
}
// WithTempDir sets the temporary directory that will be used by [Executor] for
// storing temporary files like checksums and cached remote files. By default,
// the temporary directory is set to the user's temporary directory.
func WithTempDir(tempDir *TempDir) ExecutorOption {
return &tempDirOption{tempDir}
}
type tempDirOption struct {
tempDir *TempDir
}
func (o *tempDirOption) ApplyToExecutor(e *Executor) {
e.TempDir = o.tempDir
}
// WithForce ensures that the [Executor] always runs a task, even when
// fingerprinting or prompts would normally stop it.
func WithForce(force bool) ExecutorOption {
return &forceOption{force}
}
type forceOption struct {
force bool
}
func (o *forceOption) ApplyToExecutor(e *Executor) {
e.Force = o.force
}
// WithForceAll ensures that the [Executor] always runs all tasks (including
// subtasks), even when fingerprinting or prompts would normally stop them.
func WithForceAll(forceAll bool) ExecutorOption {
return &forceAllOption{forceAll}
}
type forceAllOption struct {
forceAll bool
}
func (o *forceAllOption) ApplyToExecutor(e *Executor) {
e.ForceAll = o.forceAll
}
// WithWatch tells the [Executor] to keep running in the background and watch
// for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered.
func WithWatch(watch bool) ExecutorOption {
return &watchOption{watch}
}
type watchOption struct {
watch bool
}
func (o *watchOption) ApplyToExecutor(e *Executor) {
e.Watch = o.watch
}
// WithVerbose tells the [Executor] to output more information about the tasks
// that are run.
func WithVerbose(verbose bool) ExecutorOption {
return &verboseOption{verbose}
}
type verboseOption struct {
verbose bool
}
func (o *verboseOption) ApplyToExecutor(e *Executor) {
e.Verbose = o.verbose
}
// WithSilent tells the [Executor] to suppress all output except for the output
// of the tasks that are run.
func WithSilent(silent bool) ExecutorOption {
return &silentOption{silent}
}
type silentOption struct {
silent bool
}
func (o *silentOption) ApplyToExecutor(e *Executor) {
e.Silent = o.silent
}
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
func WithAssumeYes(assumeYes bool) ExecutorOption {
return &assumeYesOption{assumeYes}
}
type assumeYesOption struct {
assumeYes bool
}
func (o *assumeYesOption) ApplyToExecutor(e *Executor) {
e.AssumeYes = o.assumeYes
}
// WithAssumeTerm is used for testing purposes to simulate a terminal.
func WithAssumeTerm(assumeTerm bool) ExecutorOption {
return &assumeTermOption{assumeTerm}
}
type assumeTermOption struct {
assumeTerm bool
}
func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
e.AssumeTerm = o.assumeTerm
}
// WithDry tells the [Executor] to output the commands that would be run without
// actually running them.
func WithDry(dry bool) ExecutorOption {
return &dryOption{dry}
}
type dryOption struct {
dry bool
}
func (o *dryOption) ApplyToExecutor(e *Executor) {
e.Dry = o.dry
}
// WithSummary tells the [Executor] to output a summary of the given tasks
// instead of running them.
func WithSummary(summary bool) ExecutorOption {
return &summaryOption{summary}
}
type summaryOption struct {
summary bool
}
func (o *summaryOption) ApplyToExecutor(e *Executor) {
e.Summary = o.summary
}
// WithParallel tells the [Executor] to run tasks given in the same call in
// parallel.
func WithParallel(parallel bool) ExecutorOption {
return &parallelOption{parallel}
}
type parallelOption struct {
parallel bool
}
func (o *parallelOption) ApplyToExecutor(e *Executor) {
e.Parallel = o.parallel
}
// WithColor tells the [Executor] whether or not to output using colorized
// strings.
func WithColor(color bool) ExecutorOption {
return &colorOption{color}
}
type colorOption struct {
color bool
}
func (o *colorOption) ApplyToExecutor(e *Executor) {
e.Color = o.color
}
// WithConcurrency sets the maximum number of tasks that the [Executor] can run
// in parallel.
func WithConcurrency(concurrency int) ExecutorOption {
return &concurrencyOption{concurrency}
}
type concurrencyOption struct {
concurrency int
}
func (o *concurrencyOption) ApplyToExecutor(e *Executor) {
e.Concurrency = o.concurrency
}
// WithInterval sets the interval at which the [Executor] will wait for
// duplicated events before running a task.
func WithInterval(interval time.Duration) ExecutorOption {
return &intervalOption{interval}
}
type intervalOption struct {
interval time.Duration
}
func (o *intervalOption) ApplyToExecutor(e *Executor) {
e.Interval = o.interval
}
// WithOutputStyle sets the output style of the [Executor]. By default, the
// output style is set to the style defined in the Taskfile.
func WithOutputStyle(outputStyle ast.Output) ExecutorOption {
return &outputStyleOption{outputStyle}
}
type outputStyleOption struct {
outputStyle ast.Output
}
func (o *outputStyleOption) ApplyToExecutor(e *Executor) {
e.OutputStyle = o.outputStyle
}
// WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By
// default, the sorter is set to sort tasks alphabetically, but with tasks with
// no namespace (in the root Taskfile) first.
func WithTaskSorter(sorter sort.Sorter) ExecutorOption {
return &taskSorterOption{sorter}
}
type taskSorterOption struct {
sorter sort.Sorter
}
func (o *taskSorterOption) ApplyToExecutor(e *Executor) {
e.TaskSorter = o.sorter
}
// WithStdin sets the [Executor]'s standard input [io.Reader].
func WithStdin(stdin io.Reader) ExecutorOption {
return &stdinOption{stdin}
}
type stdinOption struct {
stdin io.Reader
}
func (o *stdinOption) ApplyToExecutor(e *Executor) {
e.Stdin = o.stdin
}
// WithStdout sets the [Executor]'s standard output [io.Writer].
func WithStdout(stdout io.Writer) ExecutorOption {
return &stdoutOption{stdout}
}
type stdoutOption struct {
stdout io.Writer
}
func (o *stdoutOption) ApplyToExecutor(e *Executor) {
e.Stdout = o.stdout
}
// WithStderr sets the [Executor]'s standard error [io.Writer].
func WithStderr(stderr io.Writer) ExecutorOption {
return &stderrOption{stderr}
}
type stderrOption struct {
stderr io.Writer
}
func (o *stderrOption) ApplyToExecutor(e *Executor) {
e.Stderr = o.stderr
}
// WithIO sets the [Executor]'s standard input, output, and error to the same
// [io.ReadWriter].
func WithIO(rw io.ReadWriter) ExecutorOption {
return &ioOption{rw}
}
type ioOption struct {
rw io.ReadWriter
}
func (o *ioOption) ApplyToExecutor(e *Executor) {
e.Stdin = o.rw
e.Stdout = o.rw
e.Stderr = o.rw
}
// WithVersionCheck tells the [Executor] whether or not to check the version of
func WithVersionCheck(enableVersionCheck bool) ExecutorOption {
return &versionCheckOption{enableVersionCheck}
}
type versionCheckOption struct {
enableVersionCheck bool
}
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
e.EnableVersionCheck = o.enableVersionCheck
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

49
go.mod
View File

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

147
go.sum
View File

@@ -2,19 +2,21 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
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 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -23,51 +25,53 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v2 v2.7.0 h1:WHuf0DRo63uLnldCPp9ojm3gskYwEdIIfAUVG5KhoOc=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/elliotchance/orderedmap/v2 v2.7.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 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-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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
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 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -90,42 +94,47 @@ 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= 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/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
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/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 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/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -136,15 +145,21 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.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-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-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -154,15 +169,21 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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.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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -173,5 +194,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=

23
help.go
View File

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

49
init.go
View File

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

View File

@@ -1,6 +1,7 @@
package task_test package task_test
import ( import (
"io"
"os" "os"
"testing" "testing"
@@ -8,7 +9,7 @@ import (
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
) )
func TestInitDir(t *testing.T) { func TestInit(t *testing.T) {
t.Parallel() t.Parallel()
const dir = "testdata/init" const dir = "testdata/init"
@@ -19,34 +20,12 @@ func TestInitDir(t *testing.T) {
t.Errorf("Taskfile.yml should not exist") t.Errorf("Taskfile.yml should not exist")
} }
if _, err := task.InitTaskfile(dir); err != nil { if err := task.InitTaskfile(io.Discard, dir); err != nil {
t.Error(err) t.Error(err)
} }
if _, err := os.Stat(file); err != nil { if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exist") 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) _ = os.Remove(file)
} }

View File

@@ -64,15 +64,21 @@ get_binaries() {
case "$PLATFORM" in case "$PLATFORM" in
darwin/amd64) BINARIES="task" ;; darwin/amd64) BINARIES="task" ;;
darwin/arm64) BINARIES="task" ;; darwin/arm64) BINARIES="task" ;;
darwin/arm) BINARIES="task" ;; darwin/armv5) BINARIES="task" ;;
darwin/armv6) BINARIES="task" ;;
darwin/armv7) BINARIES="task" ;;
linux/386) BINARIES="task" ;; linux/386) BINARIES="task" ;;
linux/amd64) BINARIES="task" ;; linux/amd64) BINARIES="task" ;;
linux/arm64) BINARIES="task" ;; linux/arm64) BINARIES="task" ;;
linux/arm) BINARIES="task" ;; linux/armv5) BINARIES="task" ;;
linux/armv6) BINARIES="task" ;;
linux/armv7) BINARIES="task" ;;
windows/386) BINARIES="task" ;; windows/386) BINARIES="task" ;;
windows/amd64) BINARIES="task" ;; windows/amd64) BINARIES="task" ;;
windows/arm64) BINARIES="task" ;; windows/arm64) BINARIES="task" ;;
windows/arm) BINARIES="task" ;; windows/armv5) BINARIES="task" ;;
windows/armv6) BINARIES="task" ;;
windows/armv7) BINARIES="task" ;;
*) *)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new" log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1 exit 1

View File

@@ -1,4 +1,4 @@
package task package compiler
import ( import (
"bytes" "bytes"
@@ -36,16 +36,16 @@ func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {
return c.getVariables(nil, nil, true) return c.getVariables(nil, nil, true)
} }
func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { func (c *Compiler) GetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
return c.getVariables(t, call, true) return c.getVariables(t, call, true)
} }
func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, error) {
return c.getVariables(t, call, false) return c.getVariables(t, call, false)
} }
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
result := env.GetEnviron() result := GetEnviron()
specialVars, err := c.getSpecialVars(t, call) specialVars, err := c.getSpecialVars(t, call)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -75,7 +75,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
return err return err
} }
// If the variable is already set, we can set it and return // If the variable is already set, we can set it and return
if newVar.Value != nil || newVar.Sh == nil { if newVar.Value != nil {
result.Set(k, ast.Var{Value: newVar.Value}) result.Set(k, ast.Var{Value: newVar.Value})
return nil return nil
} }
@@ -103,26 +103,18 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
taskRangeFunc = getRangeFunc(dir) taskRangeFunc = getRangeFunc(dir)
} }
for k, v := range c.TaskfileEnv.All() { if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
if err := rangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
for k, v := range c.TaskfileVars.All() { if err := c.TaskfileVars.Range(rangeFunc); err != nil {
if err := rangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
if t != nil { if t != nil {
for k, v := range t.IncludeVars.All() { if err := t.IncludeVars.Range(rangeFunc); err != nil {
if err := rangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
for k, v := range t.IncludedTaskfileVars.All() { if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
if err := taskRangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
} }
@@ -130,15 +122,11 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
return result, nil return result, nil
} }
for k, v := range call.Vars.All() { if err := call.Vars.Range(rangeFunc); err != nil {
if err := rangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
for k, v := range t.Vars.All() { if err := t.Vars.Range(taskRangeFunc); err != nil {
if err := taskRangeFunc(k, v); err != nil { return nil, err
return nil, err
}
} }
return result, nil return result, nil
@@ -196,7 +184,7 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil c.dynamicCache = nil
} }
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) { func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
allVars := map[string]string{ allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]), "TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
@@ -209,16 +197,9 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir) allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
allVars["TASKFILE"] = t.Location.Taskfile allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile) allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""
allVars["TASKFILE"] = ""
allVars["TASKFILE_DIR"] = ""
} }
if call != nil { if call != nil {
allVars["ALIAS"] = call.Task allVars["ALIAS"] = call.Task
} else {
allVars["ALIAS"] = ""
} }
return allVars, nil return allVars, nil

20
internal/compiler/env.go Normal file
View File

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

View File

@@ -3,7 +3,7 @@ package deepcopy
import ( import (
"reflect" "reflect"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v2"
) )
type Copier[T any] interface { type Copier[T any] interface {

View File

@@ -9,7 +9,6 @@ type (
// Task describes a single task // Task describes a single task
Task struct { Task struct {
Name string `json:"name"` Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"` Desc string `json:"desc"`
Summary string `json:"summary"` Summary string `json:"summary"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`

15
internal/env/env.go vendored
View File

@@ -3,26 +3,13 @@ package env
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
const taskVarPrefix = "TASK_" const taskVarPrefix = "TASK_"
// GetEnviron the all return all environment variables encapsulated on a
// ast.Vars
func GetEnviron() *ast.Vars {
m := ast.NewVars()
for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m.Set(key, ast.Var{Value: val})
}
return m
}
func Get(t *ast.Task) []string { func Get(t *ast.Task) []string {
if t.Env == nil { if t.Env == nil {
return nil return nil

View File

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

26
internal/exp/maps.go Normal file
View File

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

View File

@@ -2,16 +2,13 @@ package experiments
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/go-task/task/v3/internal/slicesext"
) )
type InvalidValueError struct { type InvalidValueError struct {
Name string Name string
AllowedValues []int AllowedValues []string
Value int Value string
} }
func (err InvalidValueError) Error() string { func (err InvalidValueError) Error() string {
@@ -19,7 +16,7 @@ func (err InvalidValueError) Error() string {
"task: Experiment %q has an invalid value %q (allowed values: %s)", "task: Experiment %q has an invalid value %q (allowed values: %s)",
err.Name, err.Name,
err.Value, err.Value,
strings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), ", "), strings.Join(err.AllowedValues, ", "),
) )
} }

View File

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

View File

@@ -0,0 +1,74 @@
package experiments_test
import (
"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 []string
value string
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: []string{"1"},
wantEnabled: false,
wantActive: true,
},
{
name: `[1] allowed, value="1"`,
allowedValues: []string{"1"},
value: "1",
wantEnabled: true,
wantActive: true,
},
{
name: `[1] allowed, value="2"`,
allowedValues: []string{"1"},
value: "2",
wantEnabled: false,
wantActive: true,
wantValid: &experiments.InvalidValueError{
Name: exampleExperiment,
AllowedValues: []string{"1"},
Value: "2",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(exampleExperimentEnv, tt.value)
x := experiments.New(exampleExperiment, tt.allowedValues...)
assert.Equal(t, exampleExperiment, x.Name)
assert.Equal(t, tt.wantEnabled, x.Enabled())
assert.Equal(t, tt.wantActive, x.Active())
assert.Equal(t, tt.wantValid, x.Valid())
})
}
}

View File

@@ -0,0 +1,85 @@
package experiments
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
)
const envPrefix = "TASK_X_"
// A set of experiments that can be enabled or disabled.
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
func init() {
readDotEnv()
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")
}
// Validate checks if any experiments have been enabled while being inactive.
// If one is found, the function returns an error.
func Validate() error {
for _, x := range List() {
if err := x.Valid(); err != nil {
return err
}
}
return nil
}
func List() []Experiment {
return xList
}
func getEnv(xName string) string {
envName := fmt.Sprintf("%s%s", envPrefix, xName)
return os.Getenv(envName)
}
func getEnvFilePath() string {
// Parse the CLI flags again to get the directory/taskfile being run
// We use a flagset here so that we can parse a subset of flags without exiting on error.
var dir, taskfile string
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
fs.Usage = func() {}
_ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory.
if dir != "" {
return filepath.Join(dir, ".env")
}
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
if taskfile != "" {
return filepath.Join(filepath.Dir(taskfile), ".env")
}
// Otherwise just use the current working directory.
return ".env"
}
func readDotEnv() {
env, _ := godotenv.Read(getEnvFilePath())
// If the env var is an experiment, set it.
for key, value := range env {
if strings.HasPrefix(key, envPrefix) {
os.Setenv(key, value)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/mocks"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -30,8 +31,8 @@ func TestIsTaskUpToDate(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
task *ast.Task task *ast.Task
setupMockStatusChecker func(m *MockStatusCheckable) setupMockStatusChecker func(m *mocks.StatusCheckable)
setupMockSourcesChecker func(m *MockSourcesCheckable) setupMockSourcesChecker func(m *mocks.SourcesCheckable)
expected bool expected bool
}{ }{
{ {
@@ -51,7 +52,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: nil, setupMockStatusChecker: nil,
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
}, },
expected: true, expected: true,
@@ -63,7 +64,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: nil, setupMockStatusChecker: nil,
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
}, },
expected: false, expected: false,
@@ -74,7 +75,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: nil, Sources: nil,
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
}, },
setupMockSourcesChecker: nil, setupMockSourcesChecker: nil,
@@ -86,10 +87,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
}, },
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
}, },
expected: true, expected: true,
@@ -100,10 +101,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
}, },
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
}, },
expected: false, expected: false,
@@ -114,7 +115,7 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: nil, Sources: nil,
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
}, },
setupMockSourcesChecker: nil, setupMockSourcesChecker: nil,
@@ -126,10 +127,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
}, },
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
}, },
expected: false, expected: false,
@@ -140,10 +141,10 @@ func TestIsTaskUpToDate(t *testing.T) {
Status: []string{"status"}, Status: []string{"status"},
Sources: []*ast.Glob{{Glob: "sources"}}, Sources: []*ast.Glob{{Glob: "sources"}},
}, },
setupMockStatusChecker: func(m *MockStatusCheckable) { setupMockStatusChecker: func(m *mocks.StatusCheckable) {
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
}, },
setupMockSourcesChecker: func(m *MockSourcesCheckable) { setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
}, },
expected: false, expected: false,
@@ -153,12 +154,12 @@ func TestIsTaskUpToDate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
mockStatusChecker := NewMockStatusCheckable(t) mockStatusChecker := mocks.NewStatusCheckable(t)
if tt.setupMockStatusChecker != nil { if tt.setupMockStatusChecker != nil {
tt.setupMockStatusChecker(mockStatusChecker) tt.setupMockStatusChecker(mockStatusChecker)
} }
mockSourcesChecker := NewMockSourcesCheckable(t) mockSourcesChecker := mocks.NewSourcesCheckable(t)
if tt.setupMockSourcesChecker != nil { if tt.setupMockSourcesChecker != nil {
tt.setupMockSourcesChecker(mockSourcesChecker) tt.setupMockSourcesChecker(mockSourcesChecker)
} }

View File

@@ -4,18 +4,14 @@ import (
"cmp" "cmp"
"log" "log"
"os" "os"
"path/filepath"
"strconv" "strconv"
"time" "time"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -42,63 +38,42 @@ Options:
` `
var ( var (
Version bool Version bool
Help bool Help bool
Init bool Init bool
Completion string Completion string
List bool List bool
ListAll bool ListAll bool
ListJson bool ListJson bool
TaskSort string TaskSort string
Status bool Status bool
NoStatus bool NoStatus bool
Insecure bool Insecure bool
Force bool Force bool
ForceAll bool ForceAll bool
Watch bool Watch bool
Verbose bool Verbose bool
Silent bool Silent bool
AssumeYes bool AssumeYes bool
Dry bool Dry bool
Summary bool Summary bool
ExitCode bool ExitCode bool
Parallel bool Parallel bool
Concurrency int Concurrency int
Dir string Dir string
Entrypoint string Entrypoint string
Output ast.Output Output ast.Output
Color bool Color bool
Interval time.Duration Interval time.Duration
Global bool Global bool
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
ClearCache bool ClearCache bool
Timeout time.Duration Timeout time.Duration
CacheExpiryDuration time.Duration
) )
func init() { func init() {
// Config files can enable experiments which alter the availability and/or
// behavior of some flags, so we need to parse the experiments before the
// flags. However, we need the --taskfile and --dir flags before we can
// parse the experiments as they can alter the location of the config files.
// Because of this circular dependency, we parse the flags twice. First, we
// get the --taskfile and --dir flags, then we parse the experiments, then
// we parse the flags again to get the full set. We use a flagset here so
// that we can parse a subset of flags without exiting on error.
var dir, entrypoint string
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
fs.StringVarP(&dir, "dir", "d", "", "")
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
fs.Usage = func() {}
_ = fs.Parse(os.Args[1:])
// Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
// Parse the rest of the flags
log.SetFlags(0) log.SetFlags(0)
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
pflag.Usage = func() { pflag.Usage = func() {
@@ -128,7 +103,7 @@ func init() {
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.") pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.") pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.") pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.") pflag.StringVarP(&Dir, "dir", "d", "", "Sets directory of execution.")
pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`) pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].") pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
@@ -154,7 +129,6 @@ func init() {
pflag.BoolVar(&Offline, "offline", offline, "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.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.")
} }
pflag.Parse() pflag.Parse()
@@ -170,7 +144,8 @@ func Validate() error {
} }
if Global && Dir != "" { if Global && Dir != "" {
return errors.New("task: You can't set both --global and --dir") log.Fatal("task: You can't set both --global and --dir")
return nil
} }
if Output.Name != "group" { if Output.Name != "group" {
@@ -185,73 +160,5 @@ func Validate() error {
} }
} }
if List && ListAll {
return errors.New("task: cannot use --list and --list-all at the same time")
}
if ListJson && !List && !ListAll {
return errors.New("task: --json only applies to --list or --list-all")
}
if NoStatus && !ListJson {
return errors.New("task: --no-status only applies to --json with --list or --list-all")
}
return nil return nil
} }
// WithFlags is a special internal functional option that is used to pass flags
// from the CLI into any constructor that accepts functional options.
func WithFlags() *flagsOption {
return &flagsOption{}
}
type flagsOption struct{}
func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
// Set the sorter
var sorter sort.Sorter
switch TaskSort {
case "none":
sorter = sort.NoSort
case "alphanumeric":
sorter = sort.AlphaNumeric
}
// Change the directory to the user's home directory if the global flag is set
dir := Dir
if Global {
home, err := os.UserHomeDir()
if err == nil {
dir = home
}
}
e.Options(
task.WithDir(dir),
task.WithForce(Force),
task.WithForceAll(ForceAll),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
task.WithAssumeYes(AssumeYes),
task.WithDry(Dry || Status),
task.WithSummary(Summary),
task.WithParallel(Parallel),
task.WithColor(Color),
task.WithConcurrency(Concurrency),
task.WithInterval(Interval),
task.WithOutputStyle(Output),
task.WithTaskSorter(sorter),
task.WithVersionCheck(true),
)
}
func (o *flagsOption) ApplyToReader(r *taskfile.Reader) {
r.Options(
taskfile.WithInsecure(Insecure),
taskfile.WithDownload(Download),
taskfile.WithOffline(Offline),
taskfile.WithCacheExpiryDuration(CacheExpiryDuration),
)
}

View File

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

View File

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

View File

View File

View File

@@ -1,51 +0,0 @@
package fsnotifyext
import (
"math"
"time"
"github.com/fsnotify/fsnotify"
)
type Deduper struct {
w *fsnotify.Watcher
waitTime time.Duration
}
func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
return &Deduper{
w: w,
waitTime: waitTime,
}
}
// GetChan returns a chan of deduplicated [fsnotify.Event].
//
// [fsnotify.Chmod] operations will be skipped.
func (d *Deduper) GetChan() <-chan fsnotify.Event {
channel := make(chan fsnotify.Event)
go func() {
timers := make(map[string]*time.Timer)
for {
event, ok := <-d.w.Events
switch {
case !ok:
return
case event.Has(fsnotify.Chmod):
continue
}
timer, ok := timers[event.String()]
if !ok {
timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
timer.Stop()
timers[event.String()] = timer
}
timer.Reset(d.waitTime)
}
}()
return channel
}

View File

@@ -12,8 +12,8 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/term" "github.com/go-task/task/v3/internal/term"
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -179,8 +179,7 @@ func TestPrintAllWithSpaces(t *testing.T) {
summary.PrintTasks(&l, summary.PrintTasks(&l,
&ast.Taskfile{Tasks: tasks}, &ast.Taskfile{Tasks: tasks},
[]string{"t1", "t2", "t3"}, []*ast.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})
)
assert.True(t, strings.HasPrefix(buffer.String(), "task: t1")) assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2") assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")

View File

@@ -1,15 +1,11 @@
package templater package templater
import ( import (
"maps"
"math/rand/v2"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
@@ -21,27 +17,62 @@ var templateFuncs template.FuncMap
func init() { func init() {
taskFuncs := template.FuncMap{ taskFuncs := template.FuncMap{
"OS": os, "OS": func() string { return runtime.GOOS },
"ARCH": arch, "ARCH": func() string { return runtime.GOARCH },
"numCPU": runtime.NumCPU, "numCPU": func() int { return runtime.NumCPU() },
"catLines": catLines, "catLines": func(s string) string {
"splitLines": splitLines, s = strings.ReplaceAll(s, "\r\n", " ")
"fromSlash": filepath.FromSlash, return strings.ReplaceAll(s, "\n", " ")
"toSlash": filepath.ToSlash, },
"exeExt": exeExt, "splitLines": func(s string) []string {
"shellQuote": shellQuote, s = strings.ReplaceAll(s, "\r\n", "\n")
"splitArgs": splitArgs, return strings.Split(s, "\n")
"IsSH": IsSH, // Deprecated },
"joinPath": filepath.Join, "fromSlash": func(path string) string {
"relPath": filepath.Rel, return filepath.FromSlash(path)
"merge": merge, },
"spew": spew.Sdump, "toSlash": func(path string) string {
"fromYaml": fromYaml, return filepath.ToSlash(path)
"mustFromYaml": mustFromYaml, },
"toYaml": toYaml, "exeExt": func() string {
"mustToYaml": mustToYaml, if runtime.GOOS == "windows" {
"uuid": uuid.New, return ".exe"
"randIntN": rand.IntN, }
return ""
},
"shellQuote": func(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
},
"splitArgs": func(s string) ([]string, error) {
return shell.Fields(s, nil)
},
// IsSH is deprecated.
"IsSH": func() bool { return true },
"joinPath": func(elem ...string) string {
return filepath.Join(elem...)
},
"relPath": func(basePath, targetPath string) (string, error) {
return filepath.Rel(basePath, targetPath)
},
"merge": func(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
for k, v := range base {
result[k] = v
}
for _, m := range v {
for k, v := range m {
result[k] = v
}
}
return result
},
"spew": func(v any) string {
return spew.Sdump(v)
},
} }
// aliases // aliases
@@ -53,80 +84,7 @@ func init() {
taskFuncs["ExeExt"] = taskFuncs["exeExt"] taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = template.FuncMap(sprig.TxtFuncMap()) templateFuncs = template.FuncMap(sprig.TxtFuncMap())
maps.Copy(templateFuncs, taskFuncs) for k, v := range taskFuncs {
} templateFuncs[k] = v
func os() string {
return runtime.GOOS
}
func arch() string {
return runtime.GOARCH
}
func catLines(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
}
func splitLines(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
}
func exeExt() string {
if runtime.GOOS == "windows" {
return ".exe"
} }
return ""
}
func shellQuote(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
}
func splitArgs(s string) ([]string, error) {
return shell.Fields(s, nil)
}
// Deprecated: now always returns true
func IsSH() bool {
return true
}
func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
}
return result
}
func fromYaml(v string) any {
output, _ := mustFromYaml(v)
return output
}
func mustFromYaml(v string) (any, error) {
var output any
err := yaml.Unmarshal([]byte(v), &output)
return output, err
}
func toYaml(v any) string {
output, _ := yaml.Marshal(v)
return string(output)
}
func mustToYaml(v any) (string, error) {
output, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(output), nil
} }

View File

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

View File

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

View File

@@ -1 +0,0 @@
3.44.1

2
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.44.1", "version": "3.41.0",
"description": "A task runner / simpler Make alternative written in Go", "description": "A task runner / simpler Make alternative written in Go",
"scripts": { "scripts": {
"postinstall": "go-npm install", "postinstall": "go-npm install",

View File

@@ -12,19 +12,16 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
return nil return nil
} }
var missingVars []errors.MissingVar var missingVars []string
for _, requiredVar := range t.Requires.Vars { for _, requiredVar := range t.Requires.Vars {
_, ok := t.Vars.Get(requiredVar.Name) _, ok := t.Vars.Get(requiredVar.Name)
if !ok { if !ok {
missingVars = append(missingVars, errors.MissingVar{ missingVars = append(missingVars, requiredVar.Name)
Name: requiredVar.Name,
AllowedValues: requiredVar.Enum,
})
} }
} }
if len(missingVars) > 0 { if len(missingVars) > 0 {
return &errors.TaskMissingRequiredVarsError{ return &errors.TaskMissingRequiredVars{
TaskName: t.Name(), TaskName: t.Name(),
MissingVars: missingVars, MissingVars: missingVars,
} }
@@ -54,7 +51,7 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
} }
if len(notAllowedValuesVars) > 0 { if len(notAllowedValuesVars) > 0 {
return &errors.TaskNotAllowedVarsError{ return &errors.TaskNotAllowedVars{
TaskName: t.Name(), TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars, NotAllowedVars: notAllowedValuesVars,
} }

View File

@@ -4,13 +4,19 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath"
"slices" "slices"
"strings"
"sync" "sync"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors" "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" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/internal/version"
@@ -18,8 +24,18 @@ import (
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) setup() error { func (e *Executor) Setup() error {
e.setupLogger() e.setupLogger()
node, err := e.getRootNode()
if err != nil {
return err
}
if err := e.setupTempDir(); err != nil {
return err
}
if err := e.readTaskfile(node); err != nil {
return err
}
e.setupFuzzyModel() e.setupFuzzyModel()
e.setupStdFiles() e.setupStdFiles()
if err := e.setupOutput(); err != nil { if err := e.setupOutput(); err != nil {
@@ -39,8 +55,37 @@ func (e *Executor) setup() error {
return nil return nil
} }
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Logger, e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if err != nil {
return nil, err
}
e.Dir = node.Dir()
return node, err
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
reader := taskfile.NewReader(
node,
e.Insecure,
e.Download,
e.Offline,
e.Timeout,
e.TempDir.Remote,
e.Logger,
)
graph, err := reader.Read()
if err != nil {
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil
}
func (e *Executor) setupFuzzyModel() { func (e *Executor) setupFuzzyModel() {
if e.Taskfile == nil { if e.Taskfile != nil {
return return
} }
@@ -48,18 +93,64 @@ func (e *Executor) setupFuzzyModel() {
model.SetThreshold(1) // because we want to build grammar based on every task name model.SetThreshold(1) // because we want to build grammar based on every task name
var words []string var words []string
for name, task := range e.Taskfile.Tasks.All(nil) { for _, taskName := range e.Taskfile.Tasks.Keys() {
if task.Internal { words = append(words, taskName)
continue
for _, task := range e.Taskfile.Tasks.Values() {
words = slices.Concat(words, task.Aliases)
} }
words = append(words, name)
words = slices.Concat(words, task.Aliases)
} }
model.Train(words) model.Train(words)
e.fuzzyModel = model e.fuzzyModel = model
} }
func (e *Executor) setupTempDir() error {
if e.TempDir != (TempDir{}) {
return nil
}
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(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.Expand(tempDir)
if err != nil {
return err
}
projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir)
e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
}
}
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.Expand(remoteDir)
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
}
return nil
}
func (e *Executor) setupStdFiles() { func (e *Executor) setupStdFiles() {
if e.Stdin == nil { if e.Stdin == nil {
e.Stdin = os.Stdin e.Stdin = os.Stdin
@@ -103,9 +194,9 @@ func (e *Executor) setupCompiler() error {
} }
} }
e.Compiler = &Compiler{ e.Compiler = &compiler.Compiler{
Dir: e.Dir, Dir: e.Dir,
Entrypoint: e.Taskfile.Location, Entrypoint: e.Entrypoint,
UserWorkingDir: e.UserWorkingDir, UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env, TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars, TaskfileVars: e.Taskfile.Vars,
@@ -115,29 +206,21 @@ func (e *Executor) setupCompiler() error {
} }
func (e *Executor) readDotEnvFiles() error { func (e *Executor) readDotEnvFiles() error {
if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {
return nil
}
if e.Taskfile.Version.LessThan(ast.V3) { if e.Taskfile.Version.LessThan(ast.V3) {
return nil return nil
} }
vars, err := e.Compiler.GetTaskfileVariables() env, err := taskfile.Dotenv(e.Compiler, e.Taskfile, e.Dir)
if err != nil { if err != nil {
return err return err
} }
env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir) err = env.Range(func(key string, value ast.Var) error {
if err != nil { if _, ok := e.Taskfile.Env.Get(key); !ok {
return err 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 return err
} }
@@ -155,7 +238,7 @@ func (e *Executor) setupConcurrencyState() {
e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len()) e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())
e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len()) e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())
for k := range e.Taskfile.Tasks.Keys(nil) { for _, k := range e.Taskfile.Tasks.Keys() {
e.taskCallCount[k] = new(int32) e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{} e.mkdirMutexMap[k] = &sync.Mutex{}
} }

View File

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

147
task.go
View File

@@ -3,15 +3,18 @@ package task
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"runtime" "runtime"
"slices" "slices"
"sync"
"sync/atomic" "sync/atomic"
"time"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors" "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/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/fingerprint"
@@ -22,6 +25,9 @@ import (
"github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
"github.com/sajari/fuzzy"
"golang.org/x/sync/errgroup"
) )
const ( const (
@@ -30,15 +36,59 @@ const (
MaximumTaskCall = 1000 MaximumTaskCall = 1000
) )
// MatchingTask represents a task that matches a given call. It includes the type TempDir struct {
// task itself and a list of wildcards that were matched. Remote string
type MatchingTask struct { Fingerprint string
Task *ast.Task }
Wildcards []string
// Executor executes a Taskfile
type Executor struct {
Taskfile *ast.Taskfile
Dir string
Entrypoint string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
Download bool
Offline bool
Timeout time.Duration
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Logger *logger.Logger
Compiler *compiler.Compiler
Output output.Output
OutputStyle ast.Output
TaskSorter sort.TaskSorter
UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
} }
// Run runs Task // Run runs Task
func (e *Executor) Run(ctx context.Context, calls ...*Call) error { func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
// check if given tasks exist // check if given tasks exist
for _, call := range calls { for _, call := range calls {
task, err := e.GetTask(call) task, err := e.GetTask(call)
@@ -100,7 +150,7 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return nil return nil
} }
func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Call, watchCalls []*Call, err error) { func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) {
for _, c := range calls { for _, c := range calls {
t, err := e.GetTask(c) t, err := e.GetTask(c)
if err != nil { if err != nil {
@@ -117,7 +167,7 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
} }
// RunTask runs a task by its name // RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *Call) error { func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
t, err := e.FastCompiledTask(call) t, err := e.FastCompiledTask(call)
if err != nil { if err != nil {
return err return err
@@ -220,13 +270,13 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
} }
var exitCode interp.ExitStatus exitCode, isExitError := interp.IsExitStatus(err)
if errors.As(err, &exitCode) { if isExitError {
if t.IgnoreError { if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue continue
} }
deferredExitCode = uint8(exitCode) deferredExitCode = exitCode
} }
if call.Indirect { if call.Indirect {
@@ -267,7 +317,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
for _, d := range t.Deps { for _, d := range t.Deps {
d := d d := d
g.Go(func() error { g.Go(func() error {
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil { if err != nil {
return err return err
} }
@@ -278,7 +328,7 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) { func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -297,15 +347,13 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
} }
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil { 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()) e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
} }
} }
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error { func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
cmd := t.Cmds[i] cmd := t.Cmds[i]
switch { switch {
@@ -313,7 +361,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
reacquire := e.releaseConcurrencyLimit() reacquire := e.releaseConcurrencyLimit()
defer reacquire() defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil { if err != nil {
return err return err
} }
@@ -356,8 +404,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
if closeErr := closer(err); closeErr != nil { if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
} }
var exitCode interp.ExitStatus if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil return nil
} }
@@ -400,50 +447,35 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return execute(ctx) return execute(ctx)
} }
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
}
// Attempt a wildcard match
for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match {
matchingTasks = append(matchingTasks, &MatchingTask{
Task: value,
Wildcards: wildcards,
})
}
}
return matchingTasks
}
// GetTask will return the task with the name matching the given call from the taskfile. // GetTask will return the task with the name matching the given call from the taskfile.
// If no task is found, it will search for tasks with a matching alias. // If no task is found, it will search for tasks with a matching alias.
// If multiple tasks contain the same alias or no matches are found an error is returned. // If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call *Call) (*ast.Task, error) { func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
// Search for a matching task // Search for a matching task
matchingTasks := e.FindMatchingTasks(call) matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
if len(matchingTasks) > 0 { switch len(matchingTasks) {
case 0: // Carry on
case 1:
if call.Vars == nil { if call.Vars == nil {
call.Vars = ast.NewVars() call.Vars = ast.NewVars()
} }
call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards}) call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
return matchingTasks[0].Task, nil return matchingTasks[0].Task, nil
default:
taskNames := make([]string, len(matchingTasks))
for i, matchingTask := range matchingTasks {
taskNames[i] = matchingTask.Task.Task
}
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: taskNames,
}
} }
// If didn't find one, search for a task with a matching alias // If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task var matchingTask *ast.Task
var aliasedTasks []string var aliasedTasks []string
for task := range e.Taskfile.Tasks.Values(nil) { for _, task := range e.Taskfile.Tasks.Values() {
if slices.Contains(task.Aliases, call.Task) { if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task) aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task matchingTask = task
@@ -479,13 +511,8 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Create an error group to wait for each task to be compiled // Create an error group to wait for each task to be compiled
var g errgroup.Group var g errgroup.Group
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = sort.AlphaNumericWithRootTasksFirst
}
// Filter tasks based on the given filter functions // Filter tasks based on the given filter functions
for task := range e.Taskfile.Tasks.Values(e.TaskSorter) { for _, task := range e.Taskfile.Tasks.Values() {
var shouldFilter bool var shouldFilter bool
for _, filter := range filters { for _, filter := range filters {
if filter(task) { if filter(task) {
@@ -500,7 +527,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
g.Go(func() error { g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task}) compiledTask, err := e.CompiledTaskForTaskList(&ast.Call{Task: tasks[i].Task})
if err != nil { if err != nil {
return err return err
} }
@@ -514,6 +541,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
return nil, err return nil, err
} }
// Sort the tasks
if e.TaskSorter == nil {
e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
}
e.TaskSorter.Sort(tasks)
return tasks, nil return tasks, nil
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -116,5 +116,14 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return nil, err 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 return rootVertex.Taskfile, nil
} }

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"iter"
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
@@ -24,7 +23,6 @@ type (
AdvancedImport bool AdvancedImport bool
Vars *Vars Vars *Vars
Flatten bool Flatten bool
Checksum string
} }
// Includes is an ordered map of namespaces to includes. // Includes is an ordered map of namespaces to includes.
Includes struct { Includes struct {
@@ -86,31 +84,19 @@ func (includes *Includes) Set(key string, value *Include) bool {
return includes.om.Set(key, value) 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 // 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 // receives the include's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned. // an error, the iteration stops and the error is returned.
func (includes *Includes) All() iter.Seq2[string, *Include] { func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil || includes.om == nil { if includes == nil || includes.om == nil {
return func(yield func(string, *Include) bool) {} return nil
} }
return includes.om.AllFromFront() for pair := includes.om.Front(); pair != nil; pair = pair.Next() {
} if err := f(pair.Key, pair.Value); err != nil {
return err
// 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() return nil
}
// 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. // UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -166,7 +152,6 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Aliases []string Aliases []string
Excludes []string Excludes []string
Vars *Vars Vars *Vars
Checksum string
} }
if err := node.Decode(&includedTaskfile); err != nil { if err := node.Decode(&includedTaskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
@@ -180,7 +165,6 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.AdvancedImport = true include.AdvancedImport = true
include.Vars = includedTaskfile.Vars include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten include.Flatten = includedTaskfile.Flatten
include.Checksum = includedTaskfile.Checksum
return nil return nil
} }
@@ -203,7 +187,5 @@ func (include *Include) DeepCopy() *Include {
AdvancedImport: include.AdvancedImport, AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(), Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten, Flatten: include.Flatten,
Aliases: deepcopy.Slice(include.Aliases),
Checksum: include.Checksum,
} }
} }

View File

@@ -1,34 +1,22 @@
package ast package ast
import ( import (
"iter" "github.com/elliotchance/orderedmap/v2"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
type ( type Matrix struct {
// Matrix is an ordered map of variable names to arrays of values. om *orderedmap.OrderedMap[string, []any]
Matrix struct { }
om *orderedmap.OrderedMap[string, *MatrixRow]
} type MatrixElement orderedmap.Element[string, []any]
// A MatrixElement is a key-value pair that is used for initializing a
// Matrix structure.
MatrixElement orderedmap.Element[string, *MatrixRow]
// A MatrixRow list of values for a matrix key or a reference to another
// variable.
MatrixRow struct {
Ref string
Value []any
}
)
func NewMatrix(els ...*MatrixElement) *Matrix { func NewMatrix(els ...*MatrixElement) *Matrix {
matrix := &Matrix{ matrix := &Matrix{
om: orderedmap.NewOrderedMap[string, *MatrixRow](), om: orderedmap.NewOrderedMap[string, []any](),
} }
for _, el := range els { for _, el := range els {
matrix.Set(el.Key, el.Value) matrix.Set(el.Key, el.Value)
@@ -43,45 +31,33 @@ func (matrix *Matrix) Len() int {
return matrix.om.Len() return matrix.om.Len()
} }
func (matrix *Matrix) Get(key string) (*MatrixRow, bool) { func (matrix *Matrix) Get(key string) ([]any, bool) {
if matrix == nil || matrix.om == nil { if matrix == nil || matrix.om == nil {
return nil, false return nil, false
} }
return matrix.om.Get(key) return matrix.om.Get(key)
} }
func (matrix *Matrix) Set(key string, value *MatrixRow) bool { func (matrix *Matrix) Set(key string, value []any) bool {
if matrix == nil { if matrix == nil {
matrix = NewMatrix() matrix = NewMatrix()
} }
if matrix.om == nil { if matrix.om == nil {
matrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]() matrix.om = orderedmap.NewOrderedMap[string, []any]()
} }
return matrix.om.Set(key, value) return matrix.om.Set(key, value)
} }
// All returns an iterator that loops over all task key-value pairs. func (matrix *Matrix) Range(f func(k string, v []any) error) error {
func (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] {
if matrix == nil || matrix.om == nil { if matrix == nil || matrix.om == nil {
return func(yield func(string, *MatrixRow) bool) {} return nil
} }
return matrix.om.AllFromFront() for pair := matrix.om.Front(); pair != nil; pair = pair.Next() {
} if err := f(pair.Key, pair.Value); err != nil {
return err
// 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() return nil
}
// Values returns an iterator that loops over all task values.
func (matrix *Matrix) Values() iter.Seq[*MatrixRow] {
if matrix == nil || matrix.om == nil {
return func(yield func(*MatrixRow) bool) {}
}
return matrix.om.Values()
} }
func (matrix *Matrix) DeepCopy() *Matrix { func (matrix *Matrix) DeepCopy() *Matrix {
@@ -103,36 +79,14 @@ func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
keyNode := node.Content[i] keyNode := node.Content[i]
valueNode := node.Content[i+1] valueNode := node.Content[i+1]
switch valueNode.Kind { // Decode the value node into a Matrix struct
case yaml.SequenceNode: var v []any
// Decode the value node into a Matrix struct if err := valueNode.Decode(&v); err != nil {
var v []any return errors.NewTaskfileDecodeError(err, node)
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the row to the ordered map
matrix.Set(keyNode.Value, &MatrixRow{
Value: v,
})
case yaml.MappingNode:
// Decode the value node into a Matrix struct
var refStruct struct {
Ref string
}
if err := valueNode.Decode(&refStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the reference to the ordered map
matrix.Set(keyNode.Value, &MatrixRow{
Ref: refStruct.Ref,
})
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("matrix values must be an array or a reference")
} }
// Add the task to the ordered map
matrix.Set(keyNode.Value, v)
} }
return nil return nil
} }

View File

@@ -2,17 +2,15 @@ package ast
import ( import (
"fmt" "fmt"
"iter"
"slices" "slices"
"strings" "strings"
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/sort"
) )
type ( type (
@@ -24,6 +22,12 @@ type (
// A TaskElement is a key-value pair that is used for initializing a Tasks // A TaskElement is a key-value pair that is used for initializing a Tasks
// structure. // structure.
TaskElement orderedmap.Element[string, *Task] 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 // NewTasks creates a new instance of Tasks and initializes it with the provided
@@ -75,53 +79,81 @@ func (tasks *Tasks) Set(key string, value *Task) bool {
return tasks.om.Set(key, value) return tasks.om.Set(key, value)
} }
// All returns an iterator that loops over all task key-value pairs in the order // Range calls the provided function for each task in the map. The function
// specified by the sorter. // receives the task's key and value as arguments. If the function returns an
func (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] { // error, the iteration stops and the error is returned.
if t == nil || t.om == nil { func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
return func(yield func(string, *Task) bool) {} if tasks == nil || tasks.om == nil {
return nil
} }
if sorter == nil { for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
return t.om.AllFromFront() if err := f(pair.Key, pair.Value); err != nil {
} return err
return func(yield func(string, *Task) bool) {
for _, key := range sorter(slices.Collect(t.om.Keys()), nil) {
el := t.om.GetElement(key)
if !yield(el.Key, el.Value) {
return
}
} }
} }
return nil
} }
// Keys returns an iterator that loops over all task keys in the order specified // Keys returns a slice of all the keys in the Tasks map.
// by the sorter. func (tasks *Tasks) Keys() []string {
func (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] { if tasks == nil {
return func(yield func(string) bool) { return nil
for k := range t.All(sorter) {
if !yield(k) {
return
}
}
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var keys []string
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
keys = append(keys, pair.Key)
}
return keys
} }
// Values returns an iterator that loops over all task values in the order // Values returns a slice of all the values in the Tasks map.
// specified by the sorter. func (tasks *Tasks) Values() []*Task {
func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] { if tasks == nil {
return func(yield func(*Task) bool) { return nil
for _, v := range t.All(sorter) {
if !yield(v) {
return
}
}
} }
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var values []*Task
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
values = append(values, pair.Value)
}
return values
}
// 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 matchingTasks []*MatchingTask
// If there is a direct match, return it
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 {
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) error { func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
defer t2.mutex.RUnlock() defer t2.mutex.RUnlock()
t2.mutex.RLock() t2.mutex.RLock()
for name, v := range t2.All(nil) { err := t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
task := v.DeepCopy() task := v.DeepCopy()
@@ -130,9 +162,9 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
task.Internal = task.Internal || (include != nil && include.Internal) task.Internal = task.Internal || (include != nil && include.Internal)
taskName := name taskName := name
// if the task is in the exclude list, don't add it to the merged taskfile // if the task is in the exclude list, don't add it to the merged taskfile and early return
if slices.Contains(include.Excludes, name) { if slices.Contains(include.Excludes, name) {
continue return nil
} }
if !include.Flatten { if !include.Flatten {
@@ -187,7 +219,9 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
} }
// Add the task to the merged taskfile // Add the task to the merged taskfile
t1.Set(taskName, task) t1.Set(taskName, task)
}
return nil
})
// If the included Taskfile has a default task, is not flattened and the // 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 // parent namespace has no task with a matching name, we can add an alias so
@@ -205,7 +239,7 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
} }
} }
return nil return err
} }
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {

View File

@@ -1,11 +1,170 @@
package ast package ast
import ( import (
"strings"
"sync"
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/experiments"
) )
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)
}
// Range calls the provided function for each variable in the map. The function
// receives the variable's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (vars *Vars) Range(f func(k string, v Var) error) error {
if vars == nil || vars.om == nil {
return nil
}
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
}
// 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 pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if pair.Value.Sh != nil && *pair.Value.Sh != "" {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
return nil
}
if pair.Value.Live != nil {
m[pair.Key] = pair.Value.Live
} else {
m[pair.Key] = pair.Value.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")
}
// Var represents either a static or dynamic variable. // Var represents either a static or dynamic variable.
type Var struct { type Var struct {
Value any Value any
@@ -16,26 +175,82 @@ type Var struct {
} }
func (v *Var) UnmarshalYAML(node *yaml.Node) error { func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Enabled() {
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables
if experiments.MapVariables.Value == "1" {
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// 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
return nil
}
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil
}
}
v.Value = value
return nil
}
// 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" {
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
switch key {
case "sh", "ref", "map":
var m struct {
Sh *string
Ref string
Map any
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
v.Value = m.Map
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
}
default:
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Value = value
return nil
}
}
}
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
key := node.Content[0].Value key := node.Content[0].Value
switch key { switch key {
case "sh", "ref", "map": case "sh", "ref":
var m struct { var m struct {
Sh *string Sh *string
Ref string Ref string
Map any
} }
if err := node.Decode(&m); err != nil { if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
} }
v.Sh = m.Sh v.Sh = m.Sh
v.Ref = m.Ref v.Ref = m.Ref
v.Value = m.Map
return nil return nil
default: default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key) return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
} }
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {

View File

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

72
taskfile/cache.go Normal file
View File

@@ -0,0 +1,72 @@
package taskfile
import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"strings"
)
type Cache struct {
dir string
}
func NewCache(dir string) (*Cache, error) {
dir = filepath.Join(dir, "remote")
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Cache{
dir: dir,
}, nil
}
func checksum(b []byte) string {
h := sha256.New()
h.Write(b)
return fmt.Sprintf("%x", h.Sum(nil))
}
func (c *Cache) write(node Node, b []byte) error {
return os.WriteFile(c.cacheFilePath(node), b, 0o644)
}
func (c *Cache) read(node Node) ([]byte, error) {
return os.ReadFile(c.cacheFilePath(node))
}
func (c *Cache) writeChecksum(node Node, checksum string) error {
return os.WriteFile(c.checksumFilePath(node), []byte(checksum), 0o644)
}
func (c *Cache) readChecksum(node Node) string {
b, _ := os.ReadFile(c.checksumFilePath(node))
return string(b)
}
func (c *Cache) key(node Node) string {
return strings.TrimRight(checksum([]byte(node.Location())), "=")
}
func (c *Cache) cacheFilePath(node Node) string {
return c.filePath(node, "yaml")
}
func (c *Cache) checksumFilePath(node Node) string {
return c.filePath(node, "checksum")
}
func (c *Cache) filePath(node Node, suffix string) string {
lastDir, filename := node.FilenameAndLastDir()
prefix := filename
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s-%s", lastDir, filename)
}
return filepath.Join(c.dir, fmt.Sprintf("%s.%s.%s", prefix, c.key(node), suffix))
}
func (c *Cache) Clear() error {
return os.RemoveAll(c.dir)
}

View File

@@ -6,12 +6,22 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func Dotenv(vars *ast.Vars, tf *ast.Taskfile, dir string) (*ast.Vars, error) { func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, error) {
if len(tf.Dotenv) == 0 {
return nil, nil
}
vars, err := c.GetTaskfileVariables()
if err != nil {
return nil, err
}
env := ast.NewVars() env := ast.NewVars()
cache := &templater.Cache{Vars: vars} cache := &templater.Cache{Vars: vars}

View File

@@ -2,71 +2,71 @@ package taskfile
import ( import (
"context" "context"
"os"
"path/filepath"
"strings" "strings"
"time"
giturls "github.com/chainguard-dev/git-urls" giturls "github.com/chainguard-dev/git-urls"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/internal/logger"
) )
type Node interface { type Node interface {
Read() ([]byte, error) Read(ctx context.Context) ([]byte, error)
Parent() Node Parent() Node
Location() string Location() string
Dir() string Dir() string
Checksum() string Remote() bool
Verify(checksum string) bool
ResolveEntrypoint(entrypoint string) (string, error) ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error) ResolveDir(dir string) (string, error)
} FilenameAndLastDir() (string, string)
type RemoteNode interface {
Node
ReadContext(ctx context.Context) ([]byte, error)
CacheKey() string
} }
func NewRootNode( func NewRootNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
timeout time.Duration,
) (Node, error) { ) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir) dir = getDefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin // If the entrypoint is "-", we read from stdin
if entrypoint == "-" { if entrypoint == "-" {
return NewStdinNode(dir) return NewStdinNode(dir)
} }
return NewNode(entrypoint, dir, insecure) return NewNode(l, entrypoint, dir, insecure, timeout)
} }
func NewNode( func NewNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
timeout time.Duration,
opts ...NodeOption, opts ...NodeOption,
) (Node, error) { ) (Node, error) {
var node Node var node Node
var err error var err error
scheme, err := getScheme(entrypoint) scheme, err := getScheme(entrypoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch scheme { switch scheme {
case "git": case "git":
node, err = NewGitNode(entrypoint, dir, insecure, opts...) node, err = NewGitNode(entrypoint, dir, insecure, opts...)
case "http", "https": case "http", "https":
node, err = NewHTTPNode(entrypoint, dir, insecure, opts...) node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
default: default:
node, err = NewFileNode(entrypoint, dir, opts...) node, err = NewFileNode(l, entrypoint, dir, opts...)
}
if _, isRemote := node.(RemoteNode); isRemote && !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")
} }
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 return node, err
} }
@@ -75,7 +75,6 @@ func getScheme(uri string) (string, error) {
if u == nil { if u == nil {
return "", err return "", err
} }
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") { if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
return "git", nil return "git", nil
} }
@@ -83,6 +82,28 @@ func getScheme(uri string) (string, error) {
if i := strings.Index(uri, "://"); i != -1 { if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil return uri[:i], nil
} }
return "", nil return "", nil
} }
func getDefaultDir(entrypoint, dir string) string {
// If the entrypoint and dir are empty, we default the directory to the current working directory
if dir == "" {
if entrypoint == "" {
wd, err := os.Getwd()
if err != nil {
return ""
}
dir = wd
}
return dir
}
// If the directory is set, ensure it is an absolute path
var err error
dir, err = filepath.Abs(dir)
if err != nil {
return ""
}
return dir
}

View File

@@ -1,20 +1,19 @@
package taskfile package taskfile
type ( type (
NodeOption func(*baseNode) NodeOption func(*BaseNode)
// baseNode is a generic node that implements the Parent() methods of the // BaseNode is a generic node that implements the Parent() methods of the
// NodeReader interface. It does not implement the Read() method and it // NodeReader interface. It does not implement the Read() method and it
// designed to be embedded in other node types so that this boilerplate code // designed to be embedded in other node types so that this boilerplate code
// does not need to be repeated. // does not need to be repeated.
baseNode struct { BaseNode struct {
parent Node parent Node
dir string dir string
checksum string
} }
) )
func NewBaseNode(dir string, opts ...NodeOption) *baseNode { func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
node := &baseNode{ node := &BaseNode{
parent: nil, parent: nil,
dir: dir, dir: dir,
} }
@@ -28,29 +27,15 @@ func NewBaseNode(dir string, opts ...NodeOption) *baseNode {
} }
func WithParent(parent Node) NodeOption { func WithParent(parent Node) NodeOption {
return func(node *baseNode) { return func(node *BaseNode) {
node.parent = parent node.parent = parent
} }
} }
func WithChecksum(checksum string) NodeOption { func (node *BaseNode) Parent() Node {
return func(node *baseNode) {
node.checksum = checksum
}
}
func (node *baseNode) Parent() Node {
return node.parent return node.parent
} }
func (node *baseNode) Dir() string { func (node *BaseNode) Dir() string {
return node.dir return node.dir
} }
func (node *baseNode) Checksum() string {
return node.checksum
}
func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum
}

View File

@@ -1,113 +0,0 @@
package taskfile
import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"time"
)
const remoteCacheDir = "remote"
type CacheNode struct {
*baseNode
source RemoteNode
}
func NewCacheNode(source RemoteNode, dir string) *CacheNode {
return &CacheNode{
baseNode: &baseNode{
dir: filepath.Join(dir, remoteCacheDir),
},
source: source,
}
}
func (node *CacheNode) Read() ([]byte, error) {
return os.ReadFile(node.Location())
}
func (node *CacheNode) Write(data []byte) error {
if err := node.CreateCacheDir(); err != nil {
return err
}
return os.WriteFile(node.Location(), data, 0o644)
}
func (node *CacheNode) ReadTimestamp() time.Time {
b, err := os.ReadFile(node.timestampPath())
if err != nil {
return time.Time{}.UTC()
}
timestamp, err := time.Parse(time.RFC3339, string(b))
if err != nil {
return time.Time{}.UTC()
}
return timestamp.UTC()
}
func (node *CacheNode) WriteTimestamp(t time.Time) error {
if err := node.CreateCacheDir(); err != nil {
return err
}
return os.WriteFile(node.timestampPath(), []byte(t.Format(time.RFC3339)), 0o644)
}
func (node *CacheNode) ReadChecksum() string {
b, _ := os.ReadFile(node.checksumPath())
return string(b)
}
func (node *CacheNode) WriteChecksum(checksum string) error {
if err := node.CreateCacheDir(); err != nil {
return err
}
return os.WriteFile(node.checksumPath(), []byte(checksum), 0o644)
}
func (node *CacheNode) CreateCacheDir() error {
if err := os.MkdirAll(node.dir, 0o755); err != nil {
return err
}
return nil
}
func (node *CacheNode) ChecksumPrompt(checksum string) string {
cachedChecksum := node.ReadChecksum()
switch {
// If the checksum doesn't exist, prompt the user to continue
case cachedChecksum == "":
return taskfileUntrustedPrompt
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
case cachedChecksum != checksum:
return taskfileChangedPrompt
default:
return ""
}
}
func (node *CacheNode) Location() string {
return node.filePath("yaml")
}
func (node *CacheNode) checksumPath() string {
return node.filePath("checksum")
}
func (node *CacheNode) timestampPath() string {
return node.filePath("timestamp")
}
func (node *CacheNode) filePath(suffix string) string {
return filepath.Join(node.dir, fmt.Sprintf("%s.%s", node.source.CacheKey(), suffix))
}
func checksum(b []byte) string {
h := sha256.New()
h.Write(b)
return fmt.Sprintf("%x", h.Sum(nil))
}

View File

@@ -1,6 +1,7 @@
package taskfile package taskfile
import ( import (
"context"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -8,33 +9,37 @@ import (
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/internal/logger"
) )
// A FileNode is a node that reads a taskfile from the local filesystem. // A FileNode is a node that reads a taskfile from the local filesystem.
type FileNode struct { type FileNode struct {
*baseNode *BaseNode
entrypoint string Entrypoint string
} }
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { func NewFileNode(l *logger.Logger, entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error var err error
base := NewBaseNode(dir, opts...) base := NewBaseNode(dir, opts...)
entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles) entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(l, entrypoint, base.dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FileNode{ return &FileNode{
baseNode: base, BaseNode: base,
entrypoint: entrypoint, Entrypoint: entrypoint,
}, nil }, nil
} }
func (node *FileNode) Location() string { func (node *FileNode) Location() string {
return node.entrypoint return node.Entrypoint
} }
func (node *FileNode) Read() ([]byte, error) { func (node *FileNode) Remote() bool {
return false
}
func (node *FileNode) Read(ctx context.Context) ([]byte, error) {
f, err := os.Open(node.Location()) f, err := os.Open(node.Location())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -43,6 +48,34 @@ func (node *FileNode) Read() ([]byte, error) {
return io.ReadAll(f) return io.ReadAll(f)
} }
// 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) {
var err error
if entrypoint != "" {
entrypoint, err = Exists(l, entrypoint)
if err != nil {
return "", "", err
}
if dir == "" {
dir = filepath.Dir(entrypoint)
}
return entrypoint, dir, nil
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return "", "", err
}
}
entrypoint, err = ExistsWalk(l, dir)
if err != nil {
return "", "", err
}
dir = filepath.Dir(entrypoint)
return entrypoint, dir, nil
}
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path // If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") { if strings.Contains(entrypoint, "://") {
@@ -52,7 +85,7 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
return entrypoint, nil return entrypoint, nil
} }
path, err := execext.ExpandLiteral(entrypoint) path, err := execext.Expand(entrypoint)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -63,12 +96,12 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another // This means that files are included relative to one another
entrypointDir := filepath.Dir(node.entrypoint) entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *FileNode) ResolveDir(dir string) (string, error) { func (node *FileNode) ResolveDir(dir string) (string, error) {
path, err := execext.ExpandLiteral(dir) path, err := execext.Expand(dir)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -79,6 +112,10 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another // This means that files are included relative to one another
entrypointDir := filepath.Dir(node.entrypoint) entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *FileNode) FilenameAndLastDir() (string, string) {
return "", filepath.Base(node.Entrypoint)
}

View File

@@ -21,8 +21,8 @@ import (
// An GitNode is a node that reads a Taskfile from a remote location via Git. // An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct { type GitNode struct {
*baseNode *BaseNode
url *url.URL URL *url.URL
rawUrl string rawUrl string
ref string ref string
path string path string
@@ -40,20 +40,23 @@ func NewGitNode(
return nil, err return nil, err
} }
basePath, path := splitURLOnDoubleSlash(u) basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref") ref := u.Query().Get("ref")
rawUrl := u.Redacted() rawUrl := u.String()
u.RawQuery = "" u.RawQuery = ""
u.Path = basePath u.Path = basePath
if u.Scheme == "http" && !insecure { if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: u.Redacted()} return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
} }
return &GitNode{ return &GitNode{
baseNode: base, BaseNode: base,
url: u, URL: u,
rawUrl: rawUrl, rawUrl: rawUrl,
ref: ref, ref: ref,
path: path, path: path,
@@ -68,15 +71,11 @@ func (node *GitNode) Remote() bool {
return true return true
} }
func (node *GitNode) Read() ([]byte, error) { func (node *GitNode) Read(_ context.Context) ([]byte, error) {
return node.ReadContext(context.Background())
}
func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New() fs := memfs.New()
storer := memory.NewStorage() storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{ _, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.url.String(), URL: node.URL.String(),
ReferenceName: plumbing.ReferenceName(node.ref), ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true, SingleBranch: true,
Depth: 1, Depth: 1,
@@ -99,7 +98,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path) dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint)) resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
if node.ref != "" { if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
} }
@@ -107,7 +106,7 @@ func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
} }
func (node *GitNode) ResolveDir(dir string) (string, error) { func (node *GitNode) ResolveDir(dir string) (string, error) {
path, err := execext.ExpandLiteral(dir) path, err := execext.Expand(dir)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -122,25 +121,6 @@ func (node *GitNode) ResolveDir(dir string) (string, error) {
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *GitNode) CacheKey() string { func (node *GitNode) FilenameAndLastDir() (string, string) {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=") return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
lastDir := filepath.Base(filepath.Dir(node.path))
prefix := filepath.Base(node.path)
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s.%s", lastDir, prefix)
}
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
}
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
x := strings.Split(u.Path, "//")
switch len(x) {
case 0:
return "", ""
case 1:
return x[0], ""
default:
return x[0], x[1]
}
} }

View File

@@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGitNode_ssh(t *testing.T) { func TestGitNode_ssh(t *testing.T) {
@@ -14,8 +13,8 @@ func TestGitNode_ssh(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path) assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -28,8 +27,8 @@ func TestGitNode_sshWithDir(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path) assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
@@ -42,8 +41,8 @@ func TestGitNode_https(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path) assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.url.String()) assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint) assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -56,38 +55,31 @@ func TestGitNode_httpsWithDir(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path) assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.url.String()) assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
} }
func TestGitNode_CacheKey(t *testing.T) { func TestGitNode_FilenameAndDir(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
entrypoint string assert.NoError(t, err)
expectedKey string filename, dir := node.FilenameAndLastDir()
}{ assert.Equal(t, "Taskfile.yml", filename)
{ assert.Equal(t, "directory", dir)
entrypoint: "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb",
},
{
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
expectedKey: "git.github.com.Taskfile.yml.39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4",
},
{
entrypoint: "https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31",
},
}
for _, tt := range tests { node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
node, err := NewGitNode(tt.entrypoint, "", false) assert.NoError(t, err)
require.NoError(t, err) filename, dir = node.FilenameAndLastDir()
key := node.CacheKey() assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, tt.expectedKey, key) assert.Equal(t, ".", dir)
}
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
} }

View File

@@ -2,28 +2,33 @@ package taskfile
import ( import (
"context" "context"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strings" "time"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
) )
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct { type HTTPNode struct {
*baseNode *BaseNode
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml) URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
entrypoint string // stores entrypoint url. used for building graph vertices.
logger *logger.Logger
timeout time.Duration
} }
func NewHTTPNode( func NewHTTPNode(
l *logger.Logger,
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
timeout time.Duration,
opts ...NodeOption, opts ...NodeOption,
) (*HTTPNode, error) { ) (*HTTPNode, error) {
base := NewBaseNode(dir, opts...) base := NewBaseNode(dir, opts...)
@@ -32,43 +37,48 @@ func NewHTTPNode(
return nil, err return nil, err
} }
if url.Scheme == "http" && !insecure { if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()} return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
} }
return &HTTPNode{ return &HTTPNode{
baseNode: base, BaseNode: base,
url: url, URL: url,
entrypoint: entrypoint,
timeout: timeout,
logger: l,
}, nil }, nil
} }
func (node *HTTPNode) Location() string { func (node *HTTPNode) Location() string {
return node.url.Redacted() return node.entrypoint
} }
func (node *HTTPNode) Read() ([]byte, error) { func (node *HTTPNode) Remote() bool {
return node.ReadContext(context.Background()) return true
} }
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) { func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, *node.url) url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequest("GET", url.String(), nil) node.URL = url
req, err := http.NewRequest("GET", node.URL.String(), nil)
if err != nil { if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.Location()} return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
} }
resp, err := http.DefaultClient.Do(req.WithContext(ctx)) resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil { if err != nil {
if ctx.Err() != nil { if errors.Is(err, context.DeadlineExceeded) {
return nil, err return nil, &errors.TaskfileNetworkTimeoutError{URI: node.URL.String(), Timeout: node.timeout}
} }
return nil, errors.TaskfileFetchFailedError{URI: node.Location()} return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{ return nil, errors.TaskfileFetchFailedError{
URI: node.Location(), URI: node.URL.String(),
HTTPStatusCode: resp.StatusCode, HTTPStatusCode: resp.StatusCode,
} }
} }
@@ -87,11 +97,11 @@ func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return node.url.ResolveReference(ref).String(), nil return node.URL.ResolveReference(ref).String(), nil
} }
func (node *HTTPNode) ResolveDir(dir string) (string, error) { func (node *HTTPNode) ResolveDir(dir string) (string, error) {
path, err := execext.ExpandLiteral(dir) path, err := execext.Expand(dir)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -110,14 +120,7 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
return filepathext.SmartJoin(parent, path), nil return filepathext.SmartJoin(parent, path), nil
} }
func (node *HTTPNode) CacheKey() string { func (node *HTTPNode) FilenameAndLastDir() (string, string) {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=") dir, filename := filepath.Split(node.entrypoint)
dir, filename := filepath.Split(node.url.Path) return filepath.Base(dir), filename
lastDir := filepath.Base(dir)
prefix := filename
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s.%s", lastDir, filename)
}
return fmt.Sprintf("http.%s.%s.%s", node.url.Host, prefix, checksum)
} }

View File

@@ -1,49 +0,0 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPNode_CacheKey(t *testing.T) {
t.Parallel()
tests := []struct {
entrypoint string
expectedKey string
}{
{
entrypoint: "https://github.com",
expectedKey: "http.github.com..996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a",
},
{
entrypoint: "https://github.com/Taskfile.yml",
expectedKey: "http.github.com.Taskfile.yml.85b3c3ad71b78dc74e404c7b4390fc13672925cb644a4d26c21b9f97c17b5fc0",
},
{
entrypoint: "https://github.com/foo",
expectedKey: "http.github.com.foo.df3158dafc823e6847d9bcaf79328446c4877405e79b100723fa6fd545ed3e2b",
},
{
entrypoint: "https://github.com/foo/Taskfile.yml",
expectedKey: "http.github.com.foo.Taskfile.yml.aea946ea7eb6f6bb4e159e8b840b6b50975927778b2e666df988c03bbf10c4c4",
},
{
entrypoint: "https://github.com/foo/bar",
expectedKey: "http.github.com.foo.bar.d3514ad1d4daedf9cc2825225070b49ebc8db47fa5177951b2a5b9994597570c",
},
{
entrypoint: "https://github.com/foo/bar/Taskfile.yml",
expectedKey: "http.github.com.bar.Taskfile.yml.b9cf01e01e47c0e96ea536e1a8bd7b3a6f6c1f1881bad438990d2bfd4ccd0ac0",
},
}
for _, tt := range tests {
node, err := NewHTTPNode(tt.entrypoint, "", false)
require.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, tt.expectedKey, key)
}
}

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