mirror of
https://github.com/go-task/task.git
synced 2025-04-17 12:06:30 +02:00
feat: support negative globs (#1324)
Co-authored-by: Andrey Nering <andrey@nering.com.br>
This commit is contained in:
parent
a7958c0e3b
commit
ec35d43677
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Added ability to exclude some files from `sources:` by using `exclude:` (#225,
|
||||||
|
#1324 by @pd93 and @andreynering).
|
||||||
- The
|
- The
|
||||||
[Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
|
[Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
|
||||||
now prefers remote files over cached ones by default (#1317, #1345 by @pd93).
|
now prefers remote files over cached ones by default (#1317, #1345 by @pd93).
|
||||||
|
@ -642,11 +642,27 @@ tasks:
|
|||||||
- public/bundle.css
|
- public/bundle.css
|
||||||
```
|
```
|
||||||
|
|
||||||
`sources` and `generates` can be files or file patterns. When given, Task will
|
`sources` and `generates` can be files or glob patterns. When given, Task will
|
||||||
compare the checksum of the source files to determine if it's necessary to run
|
compare the checksum of the source files to determine if it's necessary to run
|
||||||
the task. If not, it will just print a message like `Task "js" is up to date`.
|
the task. If not, it will just print a message like `Task "js" is up to date`.
|
||||||
|
|
||||||
If you prefer this check to be made by the modification timestamp of the files,
|
`exclude:` can also be used to exclude files from fingerprinting.
|
||||||
|
Sources are evaluated in order, so `exclude:` must come after the positive
|
||||||
|
glob it is negating.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
css:
|
||||||
|
sources:
|
||||||
|
- mysources/**/*.css
|
||||||
|
- exclude: mysources/ignoreme.css
|
||||||
|
generates:
|
||||||
|
- public/bundle.css
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer these check to be made by the modification timestamp of the files,
|
||||||
instead of its checksum (content), just set the `method` property to
|
instead of its checksum (content), just set the `method` property to
|
||||||
`timestamp`.
|
`timestamp`.
|
||||||
|
|
||||||
@ -1001,9 +1017,9 @@ This works for all types of variables.
|
|||||||
|
|
||||||
## Looping over values
|
## Looping over values
|
||||||
|
|
||||||
As of v3.28.0, Task allows you to loop over certain values and execute a
|
As of v3.28.0, Task allows you to loop over certain values and execute a command
|
||||||
command for each. There are a number of ways to do this depending on the type
|
for each. There are a number of ways to do this depending on the type of value
|
||||||
of value you want to loop over.
|
you want to loop over.
|
||||||
|
|
||||||
### Looping over a static list
|
### Looping over a static list
|
||||||
|
|
||||||
@ -1043,9 +1059,8 @@ match that glob.
|
|||||||
|
|
||||||
Source paths will always be returned as paths relative to the task directory. If
|
Source paths will always be returned as paths relative to the task directory. If
|
||||||
you need to convert this to an absolute path, you can use the built-in
|
you need to convert this to an absolute path, you can use the built-in
|
||||||
`joinPath` function.
|
`joinPath` function. There are some [special variables](/api/#special-variables)
|
||||||
There are some [special variables](/api/#special-variables) that you may find
|
that you may find useful for this.
|
||||||
useful for this.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3'
|
version: '3'
|
||||||
|
23
docs/static/schema.json
vendored
23
docs/static/schema.json
vendored
@ -88,14 +88,14 @@
|
|||||||
"description": "A list of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs.",
|
"description": "A list of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/3/glob"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generates": {
|
"generates": {
|
||||||
"description": "A list of files meant to be generated by this task. Relevant for `timestamp` method. Can be file paths or star globs.",
|
"description": "A list of files meant to be generated by this task. Relevant for `timestamp` method. Can be file paths or star globs.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/3/glob"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@ -446,6 +446,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"glob": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/3/glob_obj"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glob_obj": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exclude": {
|
||||||
|
"description": "File or glob patter to exclude from the list",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"run": {
|
"run": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["always", "once", "when_changed"]
|
"enum": ["always", "once", "when_changed"]
|
||||||
|
@ -8,16 +8,25 @@ 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/taskfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Globs(dir string, globs []string) ([]string, error) {
|
func Globs(dir string, globs []*taskfile.Glob) ([]string, error) {
|
||||||
files := make([]string, 0)
|
fileMap := make(map[string]bool)
|
||||||
for _, g := range globs {
|
for _, g := range globs {
|
||||||
f, err := Glob(dir, g)
|
matches, err := Glob(dir, g.Glob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
files = append(files, f...)
|
for _, match := range matches {
|
||||||
|
fileMap[match] = !g.Negate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files := make([]string, 0)
|
||||||
|
for file, includePath := range fileMap {
|
||||||
|
if includePath {
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(files)
|
sort.Strings(files)
|
||||||
return files, nil
|
return files, nil
|
||||||
|
@ -53,7 +53,10 @@ func (checker *ChecksumChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
|||||||
if len(t.Generates) > 0 {
|
if len(t.Generates) > 0 {
|
||||||
// For each specified 'generates' field, check whether the files actually exist
|
// For each specified 'generates' field, check whether the files actually exist
|
||||||
for _, g := range t.Generates {
|
for _, g := range t.Generates {
|
||||||
generates, err := Glob(t.Dir, g)
|
if g.Negate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generates, err := Glob(t.Dir, g.Glob)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect TRUE when no status is defined and sources are up-to-date",
|
name: "expect TRUE when no status is defined and sources are up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: nil,
|
Status: nil,
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: nil,
|
setupMockStatusChecker: nil,
|
||||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||||
@ -59,7 +59,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: nil,
|
Status: nil,
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: nil,
|
setupMockStatusChecker: nil,
|
||||||
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
setupMockSourcesChecker: func(m *mocks.SourcesCheckable) {
|
||||||
@ -83,7 +83,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect TRUE when status and sources are up-to-date",
|
name: "expect TRUE when status and sources are up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: []string{"status"},
|
Status: []string{"status"},
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
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)
|
||||||
@ -97,7 +97,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: []string{"status"},
|
Status: []string{"status"},
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
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)
|
||||||
@ -123,7 +123,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: []string{"status"},
|
Status: []string{"status"},
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
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)
|
||||||
@ -137,7 +137,7 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
name: "expect FALSE when status and sources are NOT up-to-date",
|
name: "expect FALSE when status and sources are NOT up-to-date",
|
||||||
task: &taskfile.Task{
|
task: &taskfile.Task{
|
||||||
Status: []string{"status"},
|
Status: []string{"status"},
|
||||||
Sources: []string{"sources"},
|
Sources: []*taskfile.Glob{{Glob: "sources"}},
|
||||||
},
|
},
|
||||||
setupMockStatusChecker: func(m *mocks.StatusCheckable) {
|
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)
|
||||||
|
@ -80,6 +80,21 @@ func (r *Templater) ReplaceSlice(strs []string) []string {
|
|||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Templater) ReplaceGlobs(globs []*taskfile.Glob) []*taskfile.Glob {
|
||||||
|
if r.err != nil || len(globs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
new := make([]*taskfile.Glob, len(globs))
|
||||||
|
for i, g := range globs {
|
||||||
|
new[i] = &taskfile.Glob{
|
||||||
|
Glob: r.Replace(g.Glob),
|
||||||
|
Negate: g.Negate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars {
|
func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars {
|
||||||
return r.replaceVars(vars, nil)
|
return r.replaceVars(vars, nil)
|
||||||
}
|
}
|
||||||
|
62
task_test.go
62
task_test.go
@ -1891,27 +1891,47 @@ func TestEvaluateSymlinksInPaths(t *testing.T) {
|
|||||||
Stderr: &buff,
|
Stderr: &buff,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
}
|
}
|
||||||
require.NoError(t, e.Setup())
|
tests := []struct {
|
||||||
err := e.Run(context.Background(), taskfile.Call{Task: "default"})
|
name string
|
||||||
require.NoError(t, err)
|
task string
|
||||||
assert.NotEqual(t, `task: Task "default" is up to date`, strings.TrimSpace(buff.String()))
|
expected string
|
||||||
buff.Reset()
|
}{
|
||||||
err = e.Run(context.Background(), taskfile.Call{Task: "test-sym"})
|
{
|
||||||
require.NoError(t, err)
|
name: "default (1)",
|
||||||
assert.NotEqual(t, `task: Task "test-sym" is up to date`, strings.TrimSpace(buff.String()))
|
task: "default",
|
||||||
buff.Reset()
|
expected: "task: [default] echo \"some job\"\nsome job",
|
||||||
err = e.Run(context.Background(), taskfile.Call{Task: "default"})
|
},
|
||||||
require.NoError(t, err)
|
{
|
||||||
assert.NotEqual(t, `task: Task "default" is up to date`, strings.TrimSpace(buff.String()))
|
name: "test-sym (1)",
|
||||||
buff.Reset()
|
task: "test-sym",
|
||||||
err = e.Run(context.Background(), taskfile.Call{Task: "default"})
|
expected: "task: [test-sym] echo \"shared file source changed\" > src/shared/b",
|
||||||
require.NoError(t, err)
|
},
|
||||||
assert.Equal(t, `task: Task "default" is up to date`, strings.TrimSpace(buff.String()))
|
{
|
||||||
buff.Reset()
|
name: "default (2)",
|
||||||
err = e.Run(context.Background(), taskfile.Call{Task: "reset"})
|
task: "default",
|
||||||
require.NoError(t, err)
|
expected: "task: [default] echo \"some job\"\nsome job",
|
||||||
buff.Reset()
|
},
|
||||||
err = os.RemoveAll(dir + "/.task")
|
{
|
||||||
|
name: "default (3)",
|
||||||
|
task: "default",
|
||||||
|
expected: `task: Task "default" is up to date`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reset",
|
||||||
|
task: "reset",
|
||||||
|
expected: "task: [reset] echo \"shared file source\" > src/shared/b\ntask: [reset] echo \"file source\" > src/a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
require.NoError(t, e.Setup())
|
||||||
|
err := e.Run(context.Background(), taskfile.Call{Task: test.task})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, strings.TrimSpace(buff.String()))
|
||||||
|
buff.Reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err := os.RemoveAll(dir + "/.task")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
taskfile/glob.go
Normal file
32
taskfile/glob.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package taskfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Glob struct {
|
||||||
|
Glob string
|
||||||
|
Negate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Glob) UnmarshalYAML(node *yaml.Node) error {
|
||||||
|
switch node.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
g.Glob = node.Value
|
||||||
|
return nil
|
||||||
|
case yaml.MappingNode:
|
||||||
|
var glob struct {
|
||||||
|
Exclude string
|
||||||
|
}
|
||||||
|
if err := node.Decode(&glob); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Glob = glob.Exclude
|
||||||
|
g.Negate = true
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
|
||||||
|
}
|
||||||
|
}
|
@ -19,8 +19,8 @@ type Task struct {
|
|||||||
Summary string
|
Summary string
|
||||||
Requires *Requires
|
Requires *Requires
|
||||||
Aliases []string
|
Aliases []string
|
||||||
Sources []string
|
Sources []*Glob
|
||||||
Generates []string
|
Generates []*Glob
|
||||||
Status []string
|
Status []string
|
||||||
Preconditions []*Precondition
|
Preconditions []*Precondition
|
||||||
Dir string
|
Dir string
|
||||||
@ -83,8 +83,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
Prompt string
|
Prompt string
|
||||||
Summary string
|
Summary string
|
||||||
Aliases []string
|
Aliases []string
|
||||||
Sources []string
|
Sources []*Glob
|
||||||
Generates []string
|
Generates []*Glob
|
||||||
Status []string
|
Status []string
|
||||||
Preconditions []*Precondition
|
Preconditions []*Precondition
|
||||||
Dir string
|
Dir string
|
||||||
|
4
testdata/checksum/Taskfile.yml
vendored
4
testdata/checksum/Taskfile.yml
vendored
@ -6,7 +6,9 @@ tasks:
|
|||||||
- cp ./source.txt ./generated.txt
|
- cp ./source.txt ./generated.txt
|
||||||
sources:
|
sources:
|
||||||
- ./**/glob-with-inexistent-file.txt
|
- ./**/glob-with-inexistent-file.txt
|
||||||
- ./source.txt
|
- ./*.txt
|
||||||
|
- exclude: ./ignore_me.txt
|
||||||
|
- exclude: ./generated.txt
|
||||||
generates:
|
generates:
|
||||||
- ./generated.txt
|
- ./generated.txt
|
||||||
method: checksum
|
method: checksum
|
||||||
|
1
testdata/checksum/ignore_me.txt
vendored
Normal file
1
testdata/checksum/ignore_me.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
plz ignore me
|
@ -50,8 +50,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
|||||||
Prompt: r.Replace(origTask.Prompt),
|
Prompt: r.Replace(origTask.Prompt),
|
||||||
Summary: r.Replace(origTask.Summary),
|
Summary: r.Replace(origTask.Summary),
|
||||||
Aliases: origTask.Aliases,
|
Aliases: origTask.Aliases,
|
||||||
Sources: r.ReplaceSlice(origTask.Sources),
|
Sources: r.ReplaceGlobs(origTask.Sources),
|
||||||
Generates: r.ReplaceSlice(origTask.Generates),
|
Generates: r.ReplaceGlobs(origTask.Generates),
|
||||||
Dir: r.Replace(origTask.Dir),
|
Dir: r.Replace(origTask.Dir),
|
||||||
Set: origTask.Set,
|
Set: origTask.Set,
|
||||||
Shopt: origTask.Shopt,
|
Shopt: origTask.Shopt,
|
||||||
|
7
watch.go
7
watch.go
@ -142,7 +142,12 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range task.Sources {
|
globs, err := fingerprint.Globs(task.Dir, task.Sources)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range globs {
|
||||||
files, err := fingerprint.Glob(task.Dir, s)
|
files, err := fingerprint.Glob(task.Dir, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("task: %s: %w", s, err)
|
return fmt.Errorf("task: %s: %w", s, err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user