From 347fcf9f677afd9cb0b3e8baff0bbe6d4d1ea22a Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Sat, 14 Jan 2023 12:17:36 -0800 Subject: [PATCH] fix: avoid reruns when the timestamp method is used (#977) --- docs/docs/usage.md | 6 +-- internal/status/checksum.go | 4 +- internal/status/checksum_test.go | 2 +- internal/status/timestamp.go | 74 ++++++++++++++++++++++---------- status.go | 3 ++ 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 6a73b5b8..b47c5908 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -667,9 +667,9 @@ The method `none` skips any validation and always run the task. :::info -For the `checksum` (default) method to work, it is only necessary to -inform the source files, but if you want to use the `timestamp` method, you -also need to inform the generated files with `generates`. +For the `checksum` (default) or `timestamp` method to work, it is only necessary to +inform the source files. +When the `timestamp` method is used, the last time of the running the task is considered as a generate. ::: diff --git a/internal/status/checksum.go b/internal/status/checksum.go index 6a03e147..9468ff78 100644 --- a/internal/status/checksum.go +++ b/internal/status/checksum.go @@ -109,12 +109,12 @@ func (*Checksum) Kind() string { } func (c *Checksum) checksumFilePath() string { - return filepath.Join(c.TempDir, "checksum", c.normalizeFilename(c.Task)) + return filepath.Join(c.TempDir, "checksum", NormalizeFilename(c.Task)) } var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]") // replaces invalid caracters on filenames with "-" -func (*Checksum) normalizeFilename(f string) string { +func NormalizeFilename(f string) string { return checksumFilenameRegexp.ReplaceAllString(f, "-") } diff --git a/internal/status/checksum_test.go b/internal/status/checksum_test.go index 2181ec96..b13c0eab 100644 --- a/internal/status/checksum_test.go +++ b/internal/status/checksum_test.go @@ -16,6 +16,6 @@ func TestNormalizeFilename(t *testing.T) { {"foo1bar2baz3", "foo1bar2baz3"}, } for _, test := range tests { - assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In)) + assert.Equal(t, test.Out, NormalizeFilename(test.In)) } } diff --git a/internal/status/timestamp.go b/internal/status/timestamp.go index 3801c1ac..59c351fd 100644 --- a/internal/status/timestamp.go +++ b/internal/status/timestamp.go @@ -2,20 +2,24 @@ package status import ( "os" + "path/filepath" "time" ) // Timestamp 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 } // IsUpToDate implements the Checker interface func (t *Timestamp) IsUpToDate() (bool, error) { - if len(t.Sources) == 0 || len(t.Generates) == 0 { + if len(t.Sources) == 0 { return false, nil } @@ -28,17 +32,43 @@ func (t *Timestamp) IsUpToDate() (bool, error) { return false, nil } - sourcesMaxTime, err := getMaxTime(sources...) - if err != nil || sourcesMaxTime.IsZero() { + timestampFile := t.timestampFilePath() + + // if the file exists, add the file path to the generates + // if the generate file is old, the task will be executed + _, err = os.Stat(timestampFile) + if err == nil { + generates = append(generates, timestampFile) + } else { + // create the timestamp file for the next execution when the file does not exist + if !t.Dry { + _ = os.MkdirAll(filepath.Dir(timestampFile), 0o755) + _, _ = os.Create(timestampFile) + } + } + + taskTime := time.Now() + + // compare the time of the generates and sources. If the generates are old, the task will be executed + + // get the max time of the generates + generateMaxTime, err := getMaxTime(generates...) + if err != nil || generateMaxTime.IsZero() { return false, nil } - generatesMinTime, err := getMinTime(generates...) - if err != nil || generatesMinTime.IsZero() { + // check if any of the source files is newer than the max time of the generates + shouldUpdate, err := anyFileNewerThan(sources, generateMaxTime) + if err != nil { return false, nil } - return !generatesMinTime.Before(sourcesMaxTime), nil + // modify the metadata of the file to the the current time + if !t.Dry { + _ = os.Chtimes(timestampFile, taskTime, taskTime) + } + + return !shouldUpdate, nil } func (t *Timestamp) Kind() string { @@ -64,18 +94,6 @@ func (t *Timestamp) Value() (interface{}, error) { return sourcesMaxTime, nil } -func getMinTime(files ...string) (time.Time, error) { - var t time.Time - for _, f := range files { - info, err := os.Stat(f) - if err != nil { - return time.Time{}, err - } - t = minTime(t, info.ModTime()) - } - return t, nil -} - func getMaxTime(files ...string) (time.Time, error) { var t time.Time for _, f := range files { @@ -88,11 +106,19 @@ func getMaxTime(files ...string) (time.Time, error) { return t, nil } -func minTime(a, b time.Time) time.Time { - if !a.IsZero() && a.Before(b) { - return a +// if the modification time of any of the files is newer than the the given time, returns true +// This function is lazy, as it stops when it finds a file newer than the given time +func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) { + for _, f := range files { + info, err := os.Stat(f) + if err != nil { + return false, err + } + if info.ModTime().After(givenTime) { + return true, nil + } } - return b + return false, nil } func maxTime(a, b time.Time) time.Time { @@ -106,3 +132,7 @@ func maxTime(a, b time.Time) time.Time { func (*Timestamp) OnError() error { return nil } + +func (t *Timestamp) timestampFilePath() string { + return filepath.Join(t.TempDir, "timestamp", NormalizeFilename(t.Task)) +} diff --git a/status.go b/status.go index 4d8a120e..2c2f118b 100644 --- a/status.go +++ b/status.go @@ -87,9 +87,12 @@ func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) { 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, } }