diff --git a/docs/usage.md b/docs/usage.md index 476590017d..454e1d4ad1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -302,6 +302,7 @@ tasks: cmds: - minify -o public/script.js src/js sources: + - "!src/js/node_modules/**" # don't include specify directory files - src/js/**/*.js generates: - public/script.js @@ -320,6 +321,10 @@ Task will compare the modification date/time of the 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`. +You can also add `!` before the pattern so that it will ignore all that matched files. +If you have only exclude pattern but no include pattern, all non-exclude files will +be included. + If you prefer this check to be made by the content of the files, instead of its timestamp, just set the `method` property to `checksum`. You will probably want to ignore the `.task` folder in your `.gitignore` file diff --git a/internal/status/checksum.go b/internal/status/checksum.go index 68dcd69932..d408a6df81 100644 --- a/internal/status/checksum.go +++ b/internal/status/checksum.go @@ -32,16 +32,29 @@ func (c *Checksum) IsUpToDate() (bool, error) { data, _ := ioutil.ReadFile(checksumFile) oldMd5 := strings.TrimSpace(string(data)) - sources, err := globs(c.Dir, c.Sources) + var h = md5.New() + matcher, err := NewMatcher(c.Dir, c.Sources) if err != nil { return false, err } + if err := matcher.Match(func(p string) error { + if _, err := io.WriteString(h, p); err != nil { + return err + } - newMd5, err := c.checksum(sources...) - if err != nil { - return false, nil + f, err := os.Open(p) + if err != nil { + return err + } + _, err = io.Copy(h, f) + f.Close() + return err + }); err != nil { + return false, err } + newMd5 := fmt.Sprintf("%x", h.Sum(nil)) + if !c.Dry { _ = os.MkdirAll(filepath.Join(c.Dir, ".task", "checksum"), 0755) if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil { diff --git a/internal/status/glob.go b/internal/status/glob.go index a70f663de7..2b4e4110c4 100644 --- a/internal/status/glob.go +++ b/internal/status/glob.go @@ -4,12 +4,93 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/go-task/task/v2/internal/execext" "github.com/mattn/go-zglob" ) +// Matcher represents a matcher to sources +type Matcher struct { + dir string + includes []string + excludes []string +} + +// NewMatcher creates a Matcher +func NewMatcher(dir string, globs []string) (*Matcher, error) { + var sm = Matcher{ + dir: dir, + includes: make([]string, 0, len(globs)), + excludes: make([]string, 0, len(globs)/2+1), + } + + for _, g := range globs { + isExclude := strings.HasPrefix(g, "!") + if isExclude { + g = g[1:] + } + g, err := execext.Expand(g) + if err != nil { + return nil, err + } + + if isExclude { + sm.excludes = append(sm.excludes, g) + } else { + sm.includes = append(sm.includes, g) + } + } + + return &sm, nil +} + +// Match matches the files and invoke the call back function until there is an error. +func (s Matcher) Match(callback func(p string) error) error { + return filepath.Walk(s.dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + rPath, err := filepath.Rel(s.dir, path) + if err != nil { + return err + } + + for _, g := range s.excludes { + matched, err := zglob.Match(g, rPath) + if err != nil { + return err + } else if matched { + return nil + } + } + + // if there is no includes, then all non-excludes files will be add to checksum + if len(s.includes) == 0 { + if err := callback(path); err != nil { + return err + } + } else { + for _, g := range s.includes { + matched, err := zglob.Match(g, rPath) + if err != nil { + return err + } else if matched { + if err := callback(path); err != nil { + return err + } + } + } + } + return nil + }) +} + func globs(dir string, globs []string) ([]string, error) { files := make([]string, 0) for _, g := range globs { diff --git a/internal/status/timestamp.go b/internal/status/timestamp.go index 3801c1acb3..d228bf1152 100644 --- a/internal/status/timestamp.go +++ b/internal/status/timestamp.go @@ -19,20 +19,32 @@ func (t *Timestamp) IsUpToDate() (bool, error) { return false, nil } - sources, err := globs(t.Dir, t.Sources) + var sourcesMaxTime time.Time + + matcher, err := NewMatcher(t.Dir, t.Sources) if err != nil { - return false, nil + return false, err } - generates, err := globs(t.Dir, t.Generates) - if err != nil { - return false, nil + if err := matcher.Match(func(p string) error { + info, err := os.Stat(p) + if err != nil { + return err + } + sourcesMaxTime = maxTime(sourcesMaxTime, info.ModTime()) + return nil + }); err != nil { + return false, err } - sourcesMaxTime, err := getMaxTime(sources...) if err != nil || sourcesMaxTime.IsZero() { return false, nil } + generates, err := globs(t.Dir, t.Generates) + if err != nil { + return false, nil + } + generatesMinTime, err := getMinTime(generates...) if err != nil || generatesMinTime.IsZero() { return false, nil diff --git a/status.go b/status.go index 5421c1ca9e..4d771822b0 100644 --- a/status.go +++ b/status.go @@ -3,6 +3,7 @@ package task import ( "context" "fmt" + "os" "github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/logger" @@ -50,6 +51,15 @@ func (e *Executor) statusOnError(t *taskfile.Task) error { } func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) { + dir := t.Dir + if dir == "" { + var err error + dir, err = os.Getwd() + if err != nil { + return nil, err + } + } + t.Dir = dir method := t.Method if method == "" { method = e.Taskfile.Method