mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	refactor: decouple fingerprinting from executor (#1039)
This commit is contained in:
		| @@ -13,6 +13,7 @@ | ||||
| - Fixed bug where `.task/checksum` file was sometimes not being created when | ||||
|   task also declares a `status:` | ||||
|   ([#840](https://github.com/go-task/task/issues/840), [#1035](https://github.com/go-task/task/pull/1035) by @harelwa, [#1037](https://github.com/go-task/task/pull/1037) by @pd93). | ||||
| - Refactored and decoupled fingerprinting from the main Task executor ([#1039](https://github.com/go-task/task/issues/1039) by @pd93). | ||||
| - Fixed deadlock issue when using `run: once` | ||||
|   ([#715](https://github.com/go-task/task/issues/715), [#1025](https://github.com/go-task/task/pull/1025) by @theunrepentantgeek). | ||||
|  | ||||
|   | ||||
							
								
								
									
										18
									
								
								Taskfile.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								Taskfile.yml
									
									
									
									
									
								
							| @@ -27,6 +27,24 @@ tasks: | ||||
|       GIT_COMMIT: | ||||
|         sh: git log -n 1 --format=%h | ||||
|  | ||||
|   generate: | ||||
|     desc: Runs Go generate to create mocks | ||||
|     aliases: [gen, g] | ||||
|     deps: [install:mockgen] | ||||
|     sources: | ||||
|       - "internal/fingerprint/checker.go" | ||||
|     generates: | ||||
|       - "internal/fingerprint/checker_mock.go" | ||||
|     cmds: | ||||
|       - mockgen -source=internal/fingerprint/checker.go -destination=internal/fingerprint/checker_mock.go -package=fingerprint | ||||
|  | ||||
|   install:mockgen: | ||||
|     desc: Installs mockgen; a tool to generate mock files | ||||
|     status: | ||||
|       - command -v mockgen &>/dev/null | ||||
|     cmds: | ||||
|       - go install github.com/golang/mock/mockgen@latest | ||||
|  | ||||
|   mod: | ||||
|     desc: Downloads and tidy Go modules | ||||
|     cmds: | ||||
|   | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -6,6 +6,7 @@ require ( | ||||
| 	github.com/Masterminds/semver/v3 v3.2.0 | ||||
| 	github.com/fatih/color v1.14.1 | ||||
| 	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 | ||||
| 	github.com/golang/mock v1.6.0 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/mattn/go-zglob v0.0.4 | ||||
| 	github.com/mitchellh/hashstructure/v2 v2.0.2 | ||||
|   | ||||
							
								
								
									
										27
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								go.sum
									
									
									
									
									
								
							| @@ -9,6 +9,8 @@ github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8Wlg | ||||
| github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= | ||||
| github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= | ||||
| github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= | ||||
| github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= | ||||
| github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| @@ -40,17 +42,38 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= | ||||
| golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= | ||||
| github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 h1:Ic9KukPQ7PegFzHckNiMTQXGgEszA7mY2Fn4ZMtnMbw= | ||||
| golang.org/x/exp v0.0.0-20230212135524-a684f29349b6/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= | ||||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= | ||||
| golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= | ||||
| golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
|   | ||||
							
								
								
									
										13
									
								
								help.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								help.go
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ import ( | ||||
| 	"text/tabwriter" | ||||
|  | ||||
| 	"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/taskfile" | ||||
| ) | ||||
| @@ -148,7 +149,17 @@ func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, erro | ||||
| 		Tasks: make([]editors.Task, len(tasks)), | ||||
| 	} | ||||
| 	for i, t := range tasks { | ||||
| 		upToDate, err := e.isTaskUpToDate(context.Background(), t) | ||||
| 		// Get the fingerprinting method to use | ||||
| 		method := e.Taskfile.Method | ||||
| 		if t.Method != "" { | ||||
| 			method = t.Method | ||||
| 		} | ||||
| 		upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), t, | ||||
| 			fingerprint.WithMethod(method), | ||||
| 			fingerprint.WithTempDir(e.TempDir), | ||||
| 			fingerprint.WithDry(e.Dry), | ||||
| 			fingerprint.WithLogger(e.Logger), | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										31
									
								
								internal/env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								internal/env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| package env | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| func Get(t *taskfile.Task) []string { | ||||
| 	if t.Env == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	environ := os.Environ() | ||||
|  | ||||
| 	for k, v := range t.Env.ToCacheMap() { | ||||
| 		str, isString := v.(string) | ||||
| 		if !isString { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if _, alreadySet := os.LookupEnv(k); alreadySet { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		environ = append(environ, fmt.Sprintf("%s=%s", k, str)) | ||||
| 	} | ||||
|  | ||||
| 	return environ | ||||
| } | ||||
							
								
								
									
										20
									
								
								internal/fingerprint/checker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								internal/fingerprint/checker.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| // StatusCheckable defines any type that can check if the status of a task is up-to-date. | ||||
| type StatusCheckable interface { | ||||
| 	IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) | ||||
| } | ||||
|  | ||||
| // SourcesCheckable defines any type that can check if the sources of a task are up-to-date. | ||||
| type SourcesCheckable interface { | ||||
| 	IsUpToDate(t *taskfile.Task) (bool, error) | ||||
| 	Value(t *taskfile.Task) (interface{}, error) | ||||
| 	OnError(t *taskfile.Task) error | ||||
| 	Kind() string | ||||
| } | ||||
							
								
								
									
										132
									
								
								internal/fingerprint/checker_mock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								internal/fingerprint/checker_mock.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| // Code generated by MockGen. DO NOT EDIT. | ||||
| // Source: checker.go | ||||
|  | ||||
| // Package fingerprint is a generated GoMock package. | ||||
| package fingerprint | ||||
|  | ||||
| import ( | ||||
| 	context "context" | ||||
| 	reflect "reflect" | ||||
|  | ||||
| 	taskfile "github.com/go-task/task/v3/taskfile" | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| ) | ||||
|  | ||||
| // MockStatusCheckable is a mock of StatusCheckable interface. | ||||
| type MockStatusCheckable struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockStatusCheckableMockRecorder | ||||
| } | ||||
|  | ||||
| // MockStatusCheckableMockRecorder is the mock recorder for MockStatusCheckable. | ||||
| type MockStatusCheckableMockRecorder struct { | ||||
| 	mock *MockStatusCheckable | ||||
| } | ||||
|  | ||||
| // NewMockStatusCheckable creates a new mock instance. | ||||
| func NewMockStatusCheckable(ctrl *gomock.Controller) *MockStatusCheckable { | ||||
| 	mock := &MockStatusCheckable{ctrl: ctrl} | ||||
| 	mock.recorder = &MockStatusCheckableMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockStatusCheckable) EXPECT() *MockStatusCheckableMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // IsUpToDate mocks base method. | ||||
| func (m *MockStatusCheckable) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "IsUpToDate", ctx, t) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // IsUpToDate indicates an expected call of IsUpToDate. | ||||
| func (mr *MockStatusCheckableMockRecorder) IsUpToDate(ctx, t interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStatusCheckable)(nil).IsUpToDate), ctx, t) | ||||
| } | ||||
|  | ||||
| // MockSourcesCheckable is a mock of SourcesCheckable interface. | ||||
| type MockSourcesCheckable struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockSourcesCheckableMockRecorder | ||||
| } | ||||
|  | ||||
| // MockSourcesCheckableMockRecorder is the mock recorder for MockSourcesCheckable. | ||||
| type MockSourcesCheckableMockRecorder struct { | ||||
| 	mock *MockSourcesCheckable | ||||
| } | ||||
|  | ||||
| // NewMockSourcesCheckable creates a new mock instance. | ||||
| func NewMockSourcesCheckable(ctrl *gomock.Controller) *MockSourcesCheckable { | ||||
| 	mock := &MockSourcesCheckable{ctrl: ctrl} | ||||
| 	mock.recorder = &MockSourcesCheckableMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockSourcesCheckable) EXPECT() *MockSourcesCheckableMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // IsUpToDate mocks base method. | ||||
| func (m *MockSourcesCheckable) IsUpToDate(t *taskfile.Task) (bool, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "IsUpToDate", t) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // IsUpToDate indicates an expected call of IsUpToDate. | ||||
| func (mr *MockSourcesCheckableMockRecorder) IsUpToDate(t interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockSourcesCheckable)(nil).IsUpToDate), t) | ||||
| } | ||||
|  | ||||
| // Kind mocks base method. | ||||
| func (m *MockSourcesCheckable) Kind() string { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Kind") | ||||
| 	ret0, _ := ret[0].(string) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // Kind indicates an expected call of Kind. | ||||
| func (mr *MockSourcesCheckableMockRecorder) Kind() *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockSourcesCheckable)(nil).Kind)) | ||||
| } | ||||
|  | ||||
| // OnError mocks base method. | ||||
| func (m *MockSourcesCheckable) OnError(t *taskfile.Task) error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "OnError", t) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // OnError indicates an expected call of OnError. | ||||
| func (mr *MockSourcesCheckableMockRecorder) OnError(t interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnError", reflect.TypeOf((*MockSourcesCheckable)(nil).OnError), t) | ||||
| } | ||||
|  | ||||
| // Value mocks base method. | ||||
| func (m *MockSourcesCheckable) Value(t *taskfile.Task) (interface{}, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Value", t) | ||||
| 	ret0, _ := ret[0].(interface{}) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // Value indicates an expected call of Value. | ||||
| func (mr *MockSourcesCheckableMockRecorder) Value(t interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockSourcesCheckable)(nil).Value), t) | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package status | ||||
| package fingerprint | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
							
								
								
									
										16
									
								
								internal/fingerprint/sources.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								internal/fingerprint/sources.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) { | ||||
| 	switch method { | ||||
| 	case "timestamp": | ||||
| 		return NewTimestampChecker(tempDir, dry), nil | ||||
| 	case "checksum": | ||||
| 		return NewChecksumChecker(tempDir, dry), nil | ||||
| 	case "none": | ||||
| 		return NoneChecker{}, nil | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf(`task: invalid method "%s"`, method) | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package status | ||||
| package fingerprint | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/md5" | ||||
| @@ -10,51 +10,54 @@ import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/go-task/task/v3/internal/filepathext" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
| 
 | ||||
| // Checksum validades if a task is up to date by calculating its source | ||||
| // ChecksumChecker validates if a task is up to date by calculating its source | ||||
| // files checksum | ||||
| type Checksum struct { | ||||
| 	TempDir   string | ||||
| 	TaskDir   string | ||||
| 	Task      string | ||||
| 	Sources   []string | ||||
| 	Generates []string | ||||
| 	Dry       bool | ||||
| type ChecksumChecker struct { | ||||
| 	tempDir string | ||||
| 	dry     bool | ||||
| } | ||||
| 
 | ||||
| // IsUpToDate implements the Checker interface | ||||
| func (c *Checksum) IsUpToDate() (bool, error) { | ||||
| 	if len(c.Sources) == 0 { | ||||
| func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker { | ||||
| 	return &ChecksumChecker{ | ||||
| 		tempDir: tempDir, | ||||
| 		dry:     dry, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (checker *ChecksumChecker) IsUpToDate(t *taskfile.Task) (bool, error) { | ||||
| 	if len(t.Sources) == 0 { | ||||
| 		return false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	checksumFile := c.checksumFilePath() | ||||
| 	checksumFile := checker.checksumFilePath(t) | ||||
| 
 | ||||
| 	data, _ := os.ReadFile(checksumFile) | ||||
| 	oldMd5 := strings.TrimSpace(string(data)) | ||||
| 
 | ||||
| 	sources, err := globs(c.TaskDir, c.Sources) | ||||
| 	sources, err := globs(t.Dir, t.Sources) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 
 | ||||
| 	newMd5, err := c.checksum(sources...) | ||||
| 	newMd5, err := checker.checksum(sources...) | ||||
| 	if err != nil { | ||||
| 		return false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if !c.Dry { | ||||
| 		_ = os.MkdirAll(filepathext.SmartJoin(c.TempDir, "checksum"), 0o755) | ||||
| 	if !checker.dry { | ||||
| 		_ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755) | ||||
| 		if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0o644); err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if len(c.Generates) > 0 { | ||||
| 	if len(t.Generates) > 0 { | ||||
| 		// For each specified 'generates' field, check whether the files actually exist | ||||
| 		for _, g := range c.Generates { | ||||
| 			generates, err := Glob(c.TaskDir, g) | ||||
| 		for _, g := range t.Generates { | ||||
| 			generates, err := Glob(t.Dir, g) | ||||
| 			if os.IsNotExist(err) { | ||||
| 				return false, nil | ||||
| 			} | ||||
| @@ -70,7 +73,22 @@ func (c *Checksum) IsUpToDate() (bool, error) { | ||||
| 	return oldMd5 == newMd5, nil | ||||
| } | ||||
| 
 | ||||
| func (c *Checksum) checksum(files ...string) (string, error) { | ||||
| func (checker *ChecksumChecker) Value(t *taskfile.Task) (interface{}, error) { | ||||
| 	return checker.checksum() | ||||
| } | ||||
| 
 | ||||
| func (checker *ChecksumChecker) OnError(t *taskfile.Task) error { | ||||
| 	if len(t.Sources) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return os.Remove(checker.checksumFilePath(t)) | ||||
| } | ||||
| 
 | ||||
| func (*ChecksumChecker) Kind() string { | ||||
| 	return "checksum" | ||||
| } | ||||
| 
 | ||||
| func (c *ChecksumChecker) checksum(files ...string) (string, error) { | ||||
| 	h := md5.New() | ||||
| 
 | ||||
| 	for _, f := range files { | ||||
| @@ -91,31 +109,13 @@ func (c *Checksum) checksum(files ...string) (string, error) { | ||||
| 	return fmt.Sprintf("%x", h.Sum(nil)), nil | ||||
| } | ||||
| 
 | ||||
| // Value implements the Checker Interface | ||||
| func (c *Checksum) Value() (interface{}, error) { | ||||
| 	return c.checksum() | ||||
| } | ||||
| 
 | ||||
| // OnError implements the Checker interface | ||||
| func (c *Checksum) OnError() error { | ||||
| 	if len(c.Sources) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return os.Remove(c.checksumFilePath()) | ||||
| } | ||||
| 
 | ||||
| // Kind implements the Checker Interface | ||||
| func (*Checksum) Kind() string { | ||||
| 	return "checksum" | ||||
| } | ||||
| 
 | ||||
| func (c *Checksum) checksumFilePath() string { | ||||
| 	return filepath.Join(c.TempDir, "checksum", normalizeFilename(c.Task)) | ||||
| func (checker *ChecksumChecker) checksumFilePath(t *taskfile.Task) string { | ||||
| 	return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name())) | ||||
| } | ||||
| 
 | ||||
| var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]") | ||||
| 
 | ||||
| // replaces invalid caracters on filenames with "-" | ||||
| // replaces invalid characters on filenames with "-" | ||||
| func normalizeFilename(f string) string { | ||||
| 	return checksumFilenameRegexp.ReplaceAllString(f, "-") | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package status | ||||
| package fingerprint | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
							
								
								
									
										23
									
								
								internal/fingerprint/sources_none.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								internal/fingerprint/sources_none.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import "github.com/go-task/task/v3/taskfile" | ||||
|  | ||||
| // NoneChecker is a no-op Checker. | ||||
| // It will always report that the task is not up-to-date. | ||||
| type NoneChecker struct{} | ||||
|  | ||||
| func (NoneChecker) IsUpToDate(t *taskfile.Task) (bool, error) { | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| func (NoneChecker) Value(t *taskfile.Task) (interface{}, error) { | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (NoneChecker) OnError(t *taskfile.Task) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (NoneChecker) Kind() string { | ||||
| 	return "none" | ||||
| } | ||||
| @@ -1,24 +1,29 @@ | ||||
| package status | ||||
| package fingerprint | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
| 
 | ||||
| // Timestamp checks if any source change compared with the generated files, | ||||
| // TimestampChecker checks if any source change compared with the generated files, | ||||
| // using file modifications timestamps. | ||||
| type Timestamp struct { | ||||
| 	TempDir   string | ||||
| 	Task      string | ||||
| 	Dir       string | ||||
| 	Sources   []string | ||||
| 	Generates []string | ||||
| 	Dry       bool | ||||
| type TimestampChecker struct { | ||||
| 	tempDir string | ||||
| 	dry     bool | ||||
| } | ||||
| 
 | ||||
| func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker { | ||||
| 	return &TimestampChecker{ | ||||
| 		tempDir: tempDir, | ||||
| 		dry:     dry, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // IsUpToDate implements the Checker interface | ||||
| func (t *Timestamp) IsUpToDate() (bool, error) { | ||||
| func (checker *TimestampChecker) IsUpToDate(t *taskfile.Task) (bool, error) { | ||||
| 	if len(t.Sources) == 0 { | ||||
| 		return false, nil | ||||
| 	} | ||||
| @@ -32,7 +37,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) { | ||||
| 		return false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	timestampFile := t.timestampFilePath() | ||||
| 	timestampFile := checker.timestampFilePath(t) | ||||
| 
 | ||||
| 	// If the file exists, add the file path to the generates. | ||||
| 	// If the generate file is old, the task will be executed. | ||||
| @@ -41,7 +46,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) { | ||||
| 		generates = append(generates, timestampFile) | ||||
| 	} else { | ||||
| 		// Create the timestamp file for the next execution when the file does not exist. | ||||
| 		if !t.Dry { | ||||
| 		if !checker.dry { | ||||
| 			if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil { | ||||
| 				return false, err | ||||
| 			} | ||||
| @@ -70,7 +75,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Modify the metadata of the file to the the current time. | ||||
| 	if !t.Dry { | ||||
| 	if !checker.dry { | ||||
| 		if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| @@ -79,12 +84,12 @@ func (t *Timestamp) IsUpToDate() (bool, error) { | ||||
| 	return !shouldUpdate, nil | ||||
| } | ||||
| 
 | ||||
| func (t *Timestamp) Kind() string { | ||||
| func (checker *TimestampChecker) Kind() string { | ||||
| 	return "timestamp" | ||||
| } | ||||
| 
 | ||||
| // Value implements the Checker Interface | ||||
| func (t *Timestamp) Value() (interface{}, error) { | ||||
| func (checker *TimestampChecker) Value(t *taskfile.Task) (interface{}, error) { | ||||
| 	sources, err := globs(t.Dir, t.Sources) | ||||
| 	if err != nil { | ||||
| 		return time.Now(), err | ||||
| @@ -137,10 +142,10 @@ func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) { | ||||
| } | ||||
| 
 | ||||
| // OnError implements the Checker interface | ||||
| func (*Timestamp) OnError() error { | ||||
| func (*TimestampChecker) OnError(t *taskfile.Task) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (t *Timestamp) timestampFilePath() string { | ||||
| 	return filepath.Join(t.TempDir, "timestamp", normalizeFilename(t.Task)) | ||||
| func (checker *TimestampChecker) timestampFilePath(t *taskfile.Task) string { | ||||
| 	return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task)) | ||||
| } | ||||
							
								
								
									
										36
									
								
								internal/fingerprint/status.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								internal/fingerprint/status.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/env" | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| type StatusChecker struct { | ||||
| 	logger *logger.Logger | ||||
| } | ||||
|  | ||||
| func NewStatusChecker(logger *logger.Logger) StatusCheckable { | ||||
| 	return &StatusChecker{ | ||||
| 		logger: logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (checker *StatusChecker) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) { | ||||
| 	for _, s := range t.Status { | ||||
| 		err := execext.RunCommand(ctx, &execext.RunCommandOptions{ | ||||
| 			Command: s, | ||||
| 			Dir:     t.Dir, | ||||
| 			Env:     env.Get(t), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err) | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s) | ||||
| 	} | ||||
| 	return true, nil | ||||
| } | ||||
							
								
								
									
										132
									
								
								internal/fingerprint/task.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								internal/fingerprint/task.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| 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 *taskfile.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 | ||||
| } | ||||
							
								
								
									
										174
									
								
								internal/fingerprint/task_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								internal/fingerprint/task_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| package fingerprint | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| // TruthTable | ||||
| // | ||||
| // | Status up-to-date | Sources up-to-date | Task is up-to-date | | ||||
| // | ----------------- | ------------------ | ------------------ | | ||||
| // | not set           | not set            | false              | | ||||
| // | not set           | true               | true               | | ||||
| // | not set           | false              | false              | | ||||
| // | true              | not set            | true               | | ||||
| // | true              | true               | true               | | ||||
| // | true              | false              | false              | | ||||
| // | false             | not set            | false              | | ||||
| // | false             | true               | false              | | ||||
| // | false             | false              | false              | | ||||
| func TestIsTaskUpToDate(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name                    string | ||||
| 		task                    *taskfile.Task | ||||
| 		setupMockStatusChecker  func(m *MockStatusCheckable) | ||||
| 		setupMockSourcesChecker func(m *MockSourcesCheckable) | ||||
| 		expected                bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "expect FALSE when no status or sources are defined", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  nil, | ||||
| 				Sources: nil, | ||||
| 			}, | ||||
| 			setupMockStatusChecker:  nil, | ||||
| 			setupMockSourcesChecker: nil, | ||||
| 			expected:                false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect TRUE when no status is defined and sources are up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  nil, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: nil, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect FALSE when no status is defined and sources are NOT up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  nil, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: nil, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect TRUE when status is up-to-date and sources are not defined", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: nil, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: nil, | ||||
| 			expected:                true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect TRUE when status and sources are up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect FALSE when status is NOT up-to-date and sources are not defined", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: nil, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: nil, | ||||
| 			expected:                false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil) | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "expect FALSE when status and sources are NOT up-to-date", | ||||
| 			task: &taskfile.Task{ | ||||
| 				Status:  []string{"status"}, | ||||
| 				Sources: []string{"sources"}, | ||||
| 			}, | ||||
| 			setupMockStatusChecker: func(m *MockStatusCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			setupMockSourcesChecker: func(m *MockSourcesCheckable) { | ||||
| 				m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil) | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			ctrl := gomock.NewController(t) | ||||
|  | ||||
| 			mockStatusChecker := NewMockStatusCheckable(ctrl) | ||||
| 			if tt.setupMockStatusChecker != nil { | ||||
| 				tt.setupMockStatusChecker(mockStatusChecker) | ||||
| 			} | ||||
|  | ||||
| 			mockSourcesChecker := NewMockSourcesCheckable(ctrl) | ||||
| 			if tt.setupMockSourcesChecker != nil { | ||||
| 				tt.setupMockSourcesChecker(mockSourcesChecker) | ||||
| 			} | ||||
|  | ||||
| 			result, err := IsTaskUpToDate( | ||||
| 				context.Background(), | ||||
| 				tt.task, | ||||
| 				WithStatusChecker(mockStatusChecker), | ||||
| 				WithSourcesChecker(mockSourcesChecker), | ||||
| 			) | ||||
| 			require.NoError(t, err) | ||||
| 			assert.Equal(t, tt.expected, result) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package status | ||||
|  | ||||
| // None is a no-op Checker | ||||
| type None struct{} | ||||
|  | ||||
| // IsUpToDate implements the Checker interface | ||||
| func (None) IsUpToDate() (bool, error) { | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| // Value implements the Checker interface | ||||
| func (None) Value() (interface{}, error) { | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (None) Kind() string { | ||||
| 	return "none" | ||||
| } | ||||
|  | ||||
| // OnError implements the Checker interface | ||||
| func (None) OnError() error { | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| package status | ||||
|  | ||||
| var ( | ||||
| 	_ Checker = &Timestamp{} | ||||
| 	_ Checker = &Checksum{} | ||||
| 	_ Checker = None{} | ||||
| ) | ||||
|  | ||||
| // Checker is an interface that checks if the status is up-to-date | ||||
| type Checker interface { | ||||
| 	IsUpToDate() (bool, error) | ||||
| 	Value() (interface{}, error) | ||||
| 	OnError() error | ||||
| 	Kind() string | ||||
| } | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/env" | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| @@ -19,7 +20,7 @@ func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task | ||||
| 		err := execext.RunCommand(ctx, &execext.RunCommandOptions{ | ||||
| 			Command: p.Sh, | ||||
| 			Dir:     t.Dir, | ||||
| 			Env:     getEnviron(t), | ||||
| 			Env:     env.Get(t), | ||||
| 		}) | ||||
|  | ||||
| 		if err != nil { | ||||
|   | ||||
							
								
								
									
										128
									
								
								status.go
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								status.go
									
									
									
									
									
								
							| @@ -4,20 +4,33 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/internal/status" | ||||
| 	"github.com/go-task/task/v3/internal/fingerprint" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| // Status returns an error if any the of given tasks is not up-to-date | ||||
| func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error { | ||||
| 	for _, call := range calls { | ||||
|  | ||||
| 		// Compile the task | ||||
| 		t, err := e.CompiledTask(call) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		isUpToDate, err := e.isTaskUpToDate(ctx, t) | ||||
|  | ||||
| 		// Get the fingerprinting method to use | ||||
| 		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.WithDry(e.Dry), | ||||
| 			fingerprint.WithLogger(e.Logger), | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -28,113 +41,14 @@ func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) { | ||||
| 	var statusUpToDate bool | ||||
| 	var sourcesUpToDate bool | ||||
| 	var err error | ||||
|  | ||||
| 	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 = e.isTaskUpToDateStatus(ctx, t) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// If sources is set, check if they are up-to-date | ||||
| 	if sourcesIsSet { | ||||
| 		checker, err := e.getStatusChecker(t) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 		sourcesUpToDate, err = checker.IsUpToDate() | ||||
| 		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 | ||||
| } | ||||
|  | ||||
| func (e *Executor) statusOnError(t *taskfile.Task) error { | ||||
| 	checker, err := e.getStatusChecker(t) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return checker.OnError() | ||||
| } | ||||
|  | ||||
| func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) { | ||||
| 	method := t.Method | ||||
| 	if method == "" { | ||||
| 		method = e.Taskfile.Method | ||||
| 	} | ||||
| 	switch method { | ||||
| 	case "timestamp": | ||||
| 		return e.timestampChecker(t), nil | ||||
| 	case "checksum": | ||||
| 		return e.checksumChecker(t), nil | ||||
| 	case "none": | ||||
| 		return status.None{}, nil | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf(`task: invalid method "%s"`, method) | ||||
| 	checker, err := fingerprint.NewSourcesChecker(method, e.TempDir, e.Dry) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker { | ||||
| 	return &status.Timestamp{ | ||||
| 		TempDir:   e.TempDir, | ||||
| 		Task:      t.Name(), | ||||
| 		Dir:       t.Dir, | ||||
| 		Sources:   t.Sources, | ||||
| 		Generates: t.Generates, | ||||
| 		Dry:       e.Dry, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *Executor) checksumChecker(t *taskfile.Task) status.Checker { | ||||
| 	return &status.Checksum{ | ||||
| 		TempDir:   e.TempDir, | ||||
| 		TaskDir:   t.Dir, | ||||
| 		Task:      t.Name(), | ||||
| 		Sources:   t.Sources, | ||||
| 		Generates: t.Generates, | ||||
| 		Dry:       e.Dry, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) { | ||||
| 	for _, s := range t.Status { | ||||
| 		err := execext.RunCommand(ctx, &execext.RunCommandOptions{ | ||||
| 			Command: s, | ||||
| 			Dir:     t.Dir, | ||||
| 			Env:     getEnviron(t), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err) | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s) | ||||
| 	} | ||||
| 	return true, nil | ||||
| 	return checker.OnError(t) | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								task.go
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								task.go
									
									
									
									
									
								
							| @@ -13,7 +13,9 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"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/fingerprint" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/internal/output" | ||||
| 	"github.com/go-task/task/v3/internal/slicesext" | ||||
| @@ -157,7 +159,18 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			upToDate, err := e.isTaskUpToDate(ctx, t) | ||||
| 			// Get the fingerprinting method to use | ||||
| 			method := e.Taskfile.Method | ||||
| 			if t.Method != "" { | ||||
| 				method = t.Method | ||||
| 			} | ||||
|  | ||||
| 			upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, | ||||
| 				fingerprint.WithMethod(method), | ||||
| 				fingerprint.WithTempDir(e.TempDir), | ||||
| 				fingerprint.WithDry(e.Dry), | ||||
| 				fingerprint.WithLogger(e.Logger), | ||||
| 			) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| @@ -286,7 +299,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi | ||||
| 		err = execext.RunCommand(ctx, &execext.RunCommandOptions{ | ||||
| 			Command:   cmd.Cmd, | ||||
| 			Dir:       t.Dir, | ||||
| 			Env:       getEnviron(t), | ||||
| 			Env:       env.Get(t), | ||||
| 			PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set), | ||||
| 			BashOpts:  slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt), | ||||
| 			Stdin:     e.Stdin, | ||||
| @@ -306,29 +319,6 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getEnviron(t *taskfile.Task) []string { | ||||
| 	if t.Env == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	environ := os.Environ() | ||||
|  | ||||
| 	for k, v := range t.Env.ToCacheMap() { | ||||
| 		str, isString := v.(string) | ||||
| 		if !isString { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if _, alreadySet := os.LookupEnv(k); alreadySet { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		environ = append(environ, fmt.Sprintf("%s=%s", k, str)) | ||||
| 	} | ||||
|  | ||||
| 	return environ | ||||
| } | ||||
|  | ||||
| func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute func(ctx context.Context) error) error { | ||||
| 	h, err := e.GetHash(t) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import ( | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/execext" | ||||
| 	"github.com/go-task/task/v3/internal/filepathext" | ||||
| 	"github.com/go-task/task/v3/internal/status" | ||||
| 	"github.com/go-task/task/v3/internal/fingerprint" | ||||
| 	"github.com/go-task/task/v3/internal/templater" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
| @@ -161,8 +161,11 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf | ||||
| 	} | ||||
|  | ||||
| 	if len(origTask.Status) > 0 { | ||||
| 		for _, checker := range []status.Checker{e.timestampChecker(&new), e.checksumChecker(&new)} { | ||||
| 			value, err := checker.Value() | ||||
| 		timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry) | ||||
| 		checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry) | ||||
|  | ||||
| 		for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} { | ||||
| 			value, err := checker.Value(&new) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										4
									
								
								watch.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								watch.go
									
									
									
									
									
								
							| @@ -12,8 +12,8 @@ import ( | ||||
|  | ||||
| 	"github.com/radovskyb/watcher" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/fingerprint" | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/internal/status" | ||||
| 	"github.com/go-task/task/v3/taskfile" | ||||
| ) | ||||
|  | ||||
| @@ -142,7 +142,7 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca | ||||
| 		} | ||||
|  | ||||
| 		for _, s := range task.Sources { | ||||
| 			files, err := status.Glob(task.Dir, s) | ||||
| 			files, err := fingerprint.Glob(task.Dir, s) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("task: %s: %w", s, err) | ||||
| 			} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user