mirror of
https://github.com/go-task/task.git
synced 2026-08-29 01:58:56 +02:00
refactor: centralize fingerprint method resolution in a Fingerprinter (#2924)
This commit is contained in:
@@ -17,6 +17,13 @@
|
|||||||
- Further improved fingerprinting performance on large repositories: hashing
|
- Further improved fingerprinting performance on large repositories: hashing
|
||||||
source files now reuses a single buffer, reducing memory allocations by ~98%
|
source files now reuses a single buffer, reducing memory allocations by ~98%
|
||||||
and wall-clock time by ~7% (#2925 by @vmaerten).
|
and wall-clock time by ~7% (#2925 by @vmaerten).
|
||||||
|
- Fixed the fingerprint variable (`{{.CHECKSUM}}`/`{{.TIMESTAMP}}`) ignoring a
|
||||||
|
`method:` set at the Taskfile level: the variable now follows the same method
|
||||||
|
resolution as the up-to-date check. Only the variable matching the effective
|
||||||
|
method is injected, so a task inheriting a Taskfile-level `method: timestamp`
|
||||||
|
gets `{{.TIMESTAMP}}` and no longer a `{{.CHECKSUM}}` (which now renders as an
|
||||||
|
empty string), and neither variable is injected when the effective method is
|
||||||
|
`none` (#2924 by @vmaerten).
|
||||||
|
|
||||||
## v3.52.0 - 2026-07-02
|
## v3.52.0 - 2026-07-02
|
||||||
|
|
||||||
|
|||||||
12
executor.go
12
executor.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/sajari/fuzzy"
|
"github.com/sajari/fuzzy"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/fingerprint"
|
||||||
"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/sort"
|
"github.com/go-task/task/v3/internal/sort"
|
||||||
@@ -122,6 +123,17 @@ func (e *Executor) Options(opts ...ExecutorOption) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fingerprinter is built on the fly rather than once in Setup because fields
|
||||||
|
// like Dry may be mutated between runs.
|
||||||
|
func (e *Executor) fingerprinter() *fingerprint.Fingerprinter {
|
||||||
|
return fingerprint.NewFingerprinter(
|
||||||
|
e.Taskfile.Method,
|
||||||
|
e.TempDir.Fingerprint,
|
||||||
|
e.Dry,
|
||||||
|
e.Logger,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// WithDir sets the working directory of the [Executor]. By default, the
|
// WithDir sets the working directory of the [Executor]. By default, the
|
||||||
// directory is set to the user's current working directory.
|
// directory is set to the user's current working directory.
|
||||||
func WithDir(dir string) ExecutorOption {
|
func WithDir(dir string) ExecutorOption {
|
||||||
|
|||||||
13
help.go
13
help.go
@@ -12,7 +12,6 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/editors"
|
"github.com/go-task/task/v3/internal/editors"
|
||||||
"github.com/go-task/task/v3/internal/fingerprint"
|
|
||||||
"github.com/go-task/task/v3/internal/logger"
|
"github.com/go-task/task/v3/internal/logger"
|
||||||
"github.com/go-task/task/v3/internal/sort"
|
"github.com/go-task/task/v3/internal/sort"
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
@@ -151,17 +150,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the fingerprinting method to use
|
upToDate, err := e.fingerprinter().UpToDate(context.Background(), tasks[i])
|
||||||
method := e.Taskfile.Method
|
|
||||||
if tasks[i].Method != "" {
|
|
||||||
method = tasks[i].Method
|
|
||||||
}
|
|
||||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i],
|
|
||||||
fingerprint.WithMethod(method),
|
|
||||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
|
||||||
fingerprint.WithDry(e.Dry),
|
|
||||||
fingerprint.WithLogger(e.Logger),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
148
internal/fingerprint/fingerprinter.go
Normal file
148
internal/fingerprint/fingerprinter.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/logger"
|
||||||
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FingerprinterOption func(*Fingerprinter)
|
||||||
|
|
||||||
|
// A Fingerprinter answers whether a task is up-to-date. It owns the
|
||||||
|
// resolution of the fingerprinting method and the checkers behind it.
|
||||||
|
Fingerprinter struct {
|
||||||
|
defaultMethod string
|
||||||
|
tempDir string
|
||||||
|
dry bool
|
||||||
|
logger *logger.Logger
|
||||||
|
statusChecker StatusCheckable
|
||||||
|
sourcesChecker SourcesCheckable
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithStatusChecker(checker StatusCheckable) FingerprinterOption {
|
||||||
|
return func(f *Fingerprinter) {
|
||||||
|
f.statusChecker = checker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSourcesChecker(checker SourcesCheckable) FingerprinterOption {
|
||||||
|
return func(f *Fingerprinter) {
|
||||||
|
f.sourcesChecker = checker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFingerprinter uses defaultMethod for tasks that don't declare one.
|
||||||
|
func NewFingerprinter(
|
||||||
|
defaultMethod string,
|
||||||
|
tempDir string,
|
||||||
|
dry bool,
|
||||||
|
logger *logger.Logger,
|
||||||
|
opts ...FingerprinterOption,
|
||||||
|
) *Fingerprinter {
|
||||||
|
f := &Fingerprinter{
|
||||||
|
defaultMethod: defaultMethod,
|
||||||
|
tempDir: tempDir,
|
||||||
|
dry: dry,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(f)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprinter) resolveMethod(t *ast.Task) string {
|
||||||
|
if t.Method != "" {
|
||||||
|
return t.Method
|
||||||
|
}
|
||||||
|
return f.defaultMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind names the fingerprint variable ("checksum", "timestamp" or "none") the
|
||||||
|
// resolved method injects. An invalid method is reported as "checksum" here and
|
||||||
|
// rejected by the entry points that build a checker.
|
||||||
|
func (f *Fingerprinter) Kind(t *ast.Task) string {
|
||||||
|
if f.sourcesChecker != nil {
|
||||||
|
return f.sourcesChecker.Kind()
|
||||||
|
}
|
||||||
|
switch method := f.resolveMethod(t); method {
|
||||||
|
case "timestamp", "none":
|
||||||
|
return method
|
||||||
|
default:
|
||||||
|
return "checksum"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceValue returns the value of the fingerprint variable for the given task.
|
||||||
|
// It is potentially expensive, so only call it when the task references it.
|
||||||
|
func (f *Fingerprinter) SourceValue(t *ast.Task) (any, error) {
|
||||||
|
sourcesChecker, err := f.resolveSourcesChecker(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sourcesChecker.Value(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpToDate considers both the status commands and the sources of a task; one
|
||||||
|
// that declares neither never is.
|
||||||
|
func (f *Fingerprinter) UpToDate(ctx context.Context, t *ast.Task) (bool, error) {
|
||||||
|
var statusUpToDate bool
|
||||||
|
var sourcesUpToDate bool
|
||||||
|
|
||||||
|
statusChecker := f.statusChecker
|
||||||
|
if statusChecker == nil {
|
||||||
|
statusChecker = NewStatusChecker(f.logger)
|
||||||
|
}
|
||||||
|
sourcesChecker, err := f.resolveSourcesChecker(t)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statusIsSet := len(t.Status) != 0
|
||||||
|
sourcesIsSet := len(t.Sources) != 0
|
||||||
|
|
||||||
|
if statusIsSet {
|
||||||
|
statusUpToDate, err = statusChecker.IsUpToDate(ctx, t)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourcesIsSet {
|
||||||
|
sourcesUpToDate, err = sourcesChecker.IsUpToDate(t)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusIsSet && sourcesIsSet {
|
||||||
|
return statusUpToDate && sourcesUpToDate, nil
|
||||||
|
}
|
||||||
|
if statusIsSet {
|
||||||
|
return statusUpToDate, nil
|
||||||
|
}
|
||||||
|
if sourcesIsSet {
|
||||||
|
return sourcesUpToDate, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnError lets the resolved sources checker clean up after a failed run.
|
||||||
|
func (f *Fingerprinter) OnError(t *ast.Task) error {
|
||||||
|
sourcesChecker, err := f.resolveSourcesChecker(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sourcesChecker.OnError(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveSourcesChecker is the single place where a task is mapped to a checker.
|
||||||
|
func (f *Fingerprinter) resolveSourcesChecker(t *ast.Task) (SourcesCheckable, error) {
|
||||||
|
if f.sourcesChecker != nil {
|
||||||
|
return f.sourcesChecker, nil
|
||||||
|
}
|
||||||
|
return NewSourcesChecker(f.resolveMethod(t), f.tempDir, f.dry)
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package fingerprint
|
package fingerprint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -23,7 +26,7 @@ import (
|
|||||||
// | false | not set | false |
|
// | false | not set | false |
|
||||||
// | false | true | false |
|
// | false | true | false |
|
||||||
// | false | false | false |
|
// | false | false | false |
|
||||||
func TestIsTaskUpToDate(t *testing.T) {
|
func TestFingerprinterUpToDate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -162,14 +165,94 @@ func TestIsTaskUpToDate(t *testing.T) {
|
|||||||
tt.setupMockSourcesChecker(mockSourcesChecker)
|
tt.setupMockSourcesChecker(mockSourcesChecker)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := IsTaskUpToDate(
|
f := NewFingerprinter("checksum", "", false, nil,
|
||||||
t.Context(),
|
|
||||||
tt.task,
|
|
||||||
WithStatusChecker(mockStatusChecker),
|
WithStatusChecker(mockStatusChecker),
|
||||||
WithSourcesChecker(mockSourcesChecker),
|
WithSourcesChecker(mockSourcesChecker),
|
||||||
)
|
)
|
||||||
|
result, err := f.UpToDate(t.Context(), tt.task)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The task's own method wins over the Taskfile default, for the injected
|
||||||
|
// variable as much as for the up-to-date check.
|
||||||
|
func TestFingerprinterMethodResolution(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
defaultMethod string
|
||||||
|
method string
|
||||||
|
expectedKind string
|
||||||
|
expectedValue any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "task method wins over the default",
|
||||||
|
defaultMethod: "checksum",
|
||||||
|
method: "timestamp",
|
||||||
|
expectedKind: "timestamp",
|
||||||
|
expectedValue: time.Time{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default method is inherited when the task declares none",
|
||||||
|
defaultMethod: "timestamp",
|
||||||
|
expectedKind: "timestamp",
|
||||||
|
expectedValue: time.Time{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "checksum is inherited too",
|
||||||
|
defaultMethod: "checksum",
|
||||||
|
expectedKind: "checksum",
|
||||||
|
expectedValue: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "none is inherited too",
|
||||||
|
defaultMethod: "none",
|
||||||
|
expectedKind: "none",
|
||||||
|
expectedValue: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "source.txt"), []byte("content"), 0o644))
|
||||||
|
task := &ast.Task{
|
||||||
|
Dir: dir,
|
||||||
|
Method: tt.method,
|
||||||
|
Sources: []*ast.Glob{{Glob: "source.txt"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
f := NewFingerprinter(tt.defaultMethod, t.TempDir(), true, nil)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedKind, f.Kind(task))
|
||||||
|
|
||||||
|
// A timestamp checker yields a time, the other two a string.
|
||||||
|
value, err := f.SourceValue(task)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.IsType(t, tt.expectedValue, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the entry points that need a checker reject an invalid method; Kind
|
||||||
|
// tolerates it, so that --force runs still compile.
|
||||||
|
func TestFingerprinterInvalidMethod(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const wantErr = `task: invalid method "Checksum"`
|
||||||
|
task := &ast.Task{Sources: []*ast.Glob{{Glob: "source.txt"}}}
|
||||||
|
f := NewFingerprinter("Checksum", t.TempDir(), true, nil)
|
||||||
|
|
||||||
|
assert.Equal(t, "checksum", f.Kind(task))
|
||||||
|
|
||||||
|
_, err := f.SourceValue(task)
|
||||||
|
require.ErrorIs(t, err, ErrInvalidMethod)
|
||||||
|
require.EqualError(t, err, wantErr)
|
||||||
|
_, err = f.UpToDate(t.Context(), task)
|
||||||
|
require.EqualError(t, err, wantErr)
|
||||||
|
require.EqualError(t, f.OnError(task), wantErr)
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
package fingerprint
|
package fingerprint
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidMethod lets callers that only need a fingerprint value tell a bad
|
||||||
|
// method name apart from a checker failing on the sources themselves.
|
||||||
|
var ErrInvalidMethod = errors.New("invalid method")
|
||||||
|
|
||||||
func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {
|
func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {
|
||||||
switch method {
|
switch method {
|
||||||
@@ -11,6 +19,6 @@ func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, erro
|
|||||||
case "none":
|
case "none":
|
||||||
return NoneChecker{}, nil
|
return NoneChecker{}, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
return nil, fmt.Errorf(`task: %w "%s"`, ErrInvalidMethod, method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
package fingerprint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/logger"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
CheckerOption func(*CheckerConfig)
|
|
||||||
CheckerConfig struct {
|
|
||||||
method string
|
|
||||||
dry bool
|
|
||||||
tempDir string
|
|
||||||
logger *logger.Logger
|
|
||||||
statusChecker StatusCheckable
|
|
||||||
sourcesChecker SourcesCheckable
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func WithMethod(method string) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.method = method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithDry(dry bool) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.dry = dry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTempDir(tempDir string) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.tempDir = tempDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLogger(logger *logger.Logger) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.logger = logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithStatusChecker(checker StatusCheckable) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.statusChecker = checker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithSourcesChecker(checker SourcesCheckable) CheckerOption {
|
|
||||||
return func(config *CheckerConfig) {
|
|
||||||
config.sourcesChecker = checker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsTaskUpToDate(
|
|
||||||
ctx context.Context,
|
|
||||||
t *ast.Task,
|
|
||||||
opts ...CheckerOption,
|
|
||||||
) (bool, error) {
|
|
||||||
var statusUpToDate bool
|
|
||||||
var sourcesUpToDate bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Default config
|
|
||||||
config := &CheckerConfig{
|
|
||||||
method: "none",
|
|
||||||
tempDir: "",
|
|
||||||
dry: false,
|
|
||||||
logger: nil,
|
|
||||||
statusChecker: nil,
|
|
||||||
sourcesChecker: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply functional options
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no status checker was given, set up the default one
|
|
||||||
if config.statusChecker == nil {
|
|
||||||
config.statusChecker = NewStatusChecker(config.logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no sources checker was given, set up the default one
|
|
||||||
if config.sourcesChecker == nil {
|
|
||||||
config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statusIsSet := len(t.Status) != 0
|
|
||||||
sourcesIsSet := len(t.Sources) != 0
|
|
||||||
|
|
||||||
// If status is set, check if it is up-to-date
|
|
||||||
if statusIsSet {
|
|
||||||
statusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If sources is set, check if they are up-to-date
|
|
||||||
if sourcesIsSet {
|
|
||||||
sourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If both status and sources are set, the task is up-to-date if both are up-to-date
|
|
||||||
if statusIsSet && sourcesIsSet {
|
|
||||||
return statusUpToDate && sourcesUpToDate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only status is set, the task is up-to-date if the status is up-to-date
|
|
||||||
if statusIsSet {
|
|
||||||
return statusUpToDate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only sources is set, the task is up-to-date if the sources are up-to-date
|
|
||||||
if sourcesIsSet {
|
|
||||||
return sourcesUpToDate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no status or sources are set, the task should always run
|
|
||||||
// i.e. it is never considered "up-to-date"
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
27
status.go
27
status.go
@@ -4,33 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/fingerprint"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 ...*Call) error {
|
||||||
for _, call := range calls {
|
for _, call := range calls {
|
||||||
|
|
||||||
// Compile the task
|
|
||||||
t, err := e.CompiledTask(call)
|
t, err := e.CompiledTask(call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the fingerprinting method to use
|
isUpToDate, err := e.fingerprinter().UpToDate(ctx, t)
|
||||||
method := e.Taskfile.Method
|
|
||||||
if t.Method != "" {
|
|
||||||
method = t.Method
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the task is up-to-date
|
|
||||||
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
|
||||||
fingerprint.WithMethod(method),
|
|
||||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
|
||||||
fingerprint.WithDry(e.Dry),
|
|
||||||
fingerprint.WithLogger(e.Logger),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,13 +27,5 @@ func (e *Executor) Status(ctx context.Context, calls ...*Call) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) statusOnError(t *ast.Task) error {
|
func (e *Executor) statusOnError(t *ast.Task) error {
|
||||||
method := t.Method
|
return e.fingerprinter().OnError(t)
|
||||||
if method == "" {
|
|
||||||
method = e.Taskfile.Method
|
|
||||||
}
|
|
||||||
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return checker.OnError(t)
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
task.go
13
task.go
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"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/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/slicesext"
|
"github.com/go-task/task/v3/internal/slicesext"
|
||||||
@@ -221,17 +220,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the fingerprinting method to use
|
upToDate, err := e.fingerprinter().UpToDate(ctx, t)
|
||||||
method := e.Taskfile.Method
|
|
||||||
if t.Method != "" {
|
|
||||||
method = t.Method
|
|
||||||
}
|
|
||||||
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
|
||||||
fingerprint.WithMethod(method),
|
|
||||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
|
||||||
fingerprint.WithDry(e.Dry),
|
|
||||||
fingerprint.WithLogger(e.Logger),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
75
task_test.go
75
task_test.go
@@ -653,6 +653,81 @@ func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest /
|
|||||||
require.NoError(t, err, "generated.txt should be recreated after third run")
|
require.NoError(t, err, "generated.txt should be recreated after third run")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The injected fingerprint variable follows the method the up-to-date check
|
||||||
|
// uses, including when that method comes from the Taskfile level.
|
||||||
|
func TestFingerprintVarMethod(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dir string
|
||||||
|
executorOpts []task.ExecutorOption
|
||||||
|
wantErr string
|
||||||
|
assertOutput func(t *testing.T, output string)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TIMESTAMP is injected when the method is inherited from the Taskfile",
|
||||||
|
dir: "testdata/method_taskfile_timestamp",
|
||||||
|
assertOutput: func(t *testing.T, output string) {
|
||||||
|
t.Helper()
|
||||||
|
// An unresolved variable renders as an empty string, so this
|
||||||
|
// has to match an actual timestamp, not just the prefix.
|
||||||
|
assert.Regexp(t, `ts=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`, output)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no variable is injected when the effective method is none",
|
||||||
|
dir: "testdata/method_taskfile_none",
|
||||||
|
assertOutput: func(t *testing.T, output string) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Contains(t, output, "cs=\n")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "an invalid method doesn't fail a run that skips fingerprinting",
|
||||||
|
dir: "testdata/method_invalid",
|
||||||
|
executorOpts: []task.ExecutorOption{task.WithForce(true)},
|
||||||
|
assertOutput: func(t *testing.T, output string) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Contains(t, output, "cs=[]\n")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "an invalid method is still reported by the up-to-date check",
|
||||||
|
dir: "testdata/method_invalid",
|
||||||
|
wantErr: `task: invalid method "checksums"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_ = os.RemoveAll(filepathext.SmartJoin(tt.dir, ".task"))
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
opts := append([]task.ExecutorOption{
|
||||||
|
task.WithDir(tt.dir),
|
||||||
|
task.WithStdout(&buff),
|
||||||
|
task.WithStderr(&buff),
|
||||||
|
task.WithTempDir(task.TempDir{
|
||||||
|
Remote: filepathext.SmartJoin(tt.dir, ".task"),
|
||||||
|
Fingerprint: filepathext.SmartJoin(tt.dir, ".task"),
|
||||||
|
}),
|
||||||
|
}, tt.executorOpts...)
|
||||||
|
e := task.NewExecutor(opts...)
|
||||||
|
require.NoError(t, e.Setup())
|
||||||
|
|
||||||
|
err := e.Run(t.Context(), &task.Call{Task: "build"})
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
tt.assertOutput(t, buff.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeFile(t *testing.T, dir, name, content string) {
|
func writeFile(t *testing.T, dir, name, content string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, name), []byte(content), 0o644))
|
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, name), []byte(content), 0o644))
|
||||||
|
|||||||
9
testdata/method_invalid/Taskfile.yml
vendored
Normal file
9
testdata/method_invalid/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
method: checksums # typo: not a valid method
|
||||||
|
cmds:
|
||||||
|
- echo "cs=[{{.CHECKSUM}}]"
|
||||||
|
sources:
|
||||||
|
- ./source.txt
|
||||||
1
testdata/method_invalid/source.txt
vendored
Normal file
1
testdata/method_invalid/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
x
|
||||||
10
testdata/method_taskfile_none/Taskfile.yml
vendored
Normal file
10
testdata/method_taskfile_none/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
method: none
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
cmds:
|
||||||
|
- echo "cs={{.CHECKSUM}}"
|
||||||
|
sources:
|
||||||
|
- ./source.txt
|
||||||
1
testdata/method_taskfile_none/source.txt
vendored
Normal file
1
testdata/method_taskfile_none/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
source
|
||||||
10
testdata/method_taskfile_timestamp/Taskfile.yml
vendored
Normal file
10
testdata/method_taskfile_timestamp/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
method: timestamp
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
cmds:
|
||||||
|
- echo "ts={{.TIMESTAMP}}"
|
||||||
|
sources:
|
||||||
|
- ./source.txt
|
||||||
1
testdata/method_taskfile_timestamp/source.txt
vendored
Normal file
1
testdata/method_taskfile_timestamp/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
source
|
||||||
31
variables.go
31
variables.go
@@ -208,25 +208,24 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(origTask.Sources) > 0 && origTask.Method != "none" {
|
if len(origTask.Sources) > 0 {
|
||||||
var checker fingerprint.SourcesCheckable
|
fingerprinter := e.fingerprinter()
|
||||||
|
kind := fingerprinter.Kind(&new)
|
||||||
if origTask.Method == "timestamp" {
|
if kind != "none" && origTask.ReferencesFingerprintVar(kind) {
|
||||||
checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
|
// An invalid method must not fail compilation: --force skips
|
||||||
} else {
|
// fingerprinting altogether, and the up-to-date check reports it
|
||||||
checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
|
// on every other path.
|
||||||
}
|
value, err := fingerprinter.SourceValue(&new)
|
||||||
|
if err != nil && !errors.Is(err, fingerprint.ErrInvalidMethod) {
|
||||||
if origTask.ReferencesFingerprintVar(checker.Kind()) {
|
|
||||||
value, err := checker.Value(&new)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})
|
if err == nil {
|
||||||
|
vars.Set(strings.ToUpper(kind), ast.Var{Live: value})
|
||||||
|
|
||||||
// Adding new variables, requires us to refresh the templaters
|
// Adding new variables, requires us to refresh the templaters
|
||||||
// cache of the the values manually
|
// cache of the the values manually
|
||||||
cache.ResetCache()
|
cache.ResetCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user