Repository: magefile/mage
Pull request #54 (2017-10-09 07:07:53)

Add mg.Target
This function allows specifying files/directories as targets and returns true when the targets are met and false otherwise.

Targets are met if the ModTime of src is older than any ModTime of any  of the specified targets.

For recursive checks of directory with its file contents set recusive argument to true

gernest: @natefinch @platelk  please check this again, any notes are welcome.
gernest: Done

Patch:
diff --git a/target/target.go b/target/target.go
new file mode 100644
index 0000000..a2866e9
--- /dev/null
+++ b/target/target.go
@@ -0,0 +1,122 @@
+package target
+
+import (
+	"os"
+	"path/filepath"
+	"time"
+)
+
+// Path reports if any of the sources have been modified more recently
+// than the destination.  Path does not descend into directories, it literally
+// just checks the modtime of each thing you pass to it.
+func Path(dst string, sources ...string) (bool, error) {
+	stat, err := os.Stat(dst)
+	if err != nil {
+		return false, err
+	}
+	srcTime := stat.ModTime()
+	dt, err := loadTargets(sources)
+	if err != nil {
+		return false, err
+	}
+	t := dt.modTime()
+	if t.After(srcTime) {
+		return true, nil
+	}
+	return false, nil
+}
+
+// Dir reports whether any of the sources have been modified
+// more recently than the destination.  If a source or destination is
+// a directory, modtimes of files under those directories are compared
+// instead.
+func Dir(dst string, sources ...string) (bool, error) {
+	stat, err := os.Stat(dst)
+	if err != nil {
+		return false, err
+	}
+	srcTime := stat.ModTime()
+	if stat.IsDir() {
+		srcTime, err = calDirModTimeRecursive(stat)
+		if err != nil {
+			return false, err
+		}
+	}
+	dt, err := loadTargets(sources)
+	if err != nil {
+		return false, err
+	}
+	t, err := dt.modTimeDir()
+	if err != nil {
+		return false, err
+	}
+	if t.After(srcTime) {
+		return true, nil
+	}
+	return false, nil
+}
+
+func calDirModTimeRecursive(dir os.FileInfo) (time.Time, error) {
+	t := dir.ModTime()
+	ferr := filepath.Walk(dir.Name(), func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if info.ModTime().After(t) {
+			t = info.ModTime()
+		}
+		return nil
+	})
+	if ferr != nil {
+		return time.Time{}, ferr
+	}
+	return t, nil
+}
+
+type depTargets struct {
+	src    []os.FileInfo
+	hasdir bool
+	latest time.Time
+}
+
+func loadTargets(targets []string) (*depTargets, error) {
+	d := &depTargets{}
+	for _, v := range targets {
+		stat, err := os.Stat(v)
+		if err != nil {
+			return nil, err
+		}
+		if stat.IsDir() {
+			d.hasdir = true
+		}
+		d.src = append(d.src, stat)
+		if stat.ModTime().After(d.latest) {
+			d.latest = stat.ModTime()
+		}
+	}
+	return d, nil
+}
+
+func (d *depTargets) modTime() time.Time {
+	return d.latest
+}
+
+func (d *depTargets) modTimeDir() (time.Time, error) {
+	if !d.hasdir {
+		return d.latest, nil
+	}
+	var err error
+	for _, i := range d.src {
+		t := i.ModTime()
+		if i.IsDir() {
+			t, err = calDirModTimeRecursive(i)
+			if err != nil {
+				return time.Time{}, err
+			}
+		}
+		if t.After(d.latest) {
+			d.latest = t
+		}
+	}
+	return d.latest, nil
+}


Test patch:
diff --git a/target/target_test.go b/target/target_test.go
new file mode 100644
index 0000000..c3434ef
--- /dev/null
+++ b/target/target_test.go
@@ -0,0 +1,63 @@
+package target
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+	"time"
+)
+
+func TestPath(t *testing.T) {
+	t.Parallel()
+	dirs := []string{"testdata/src_dir", "testdata/target_dir"}
+	for _, d := range dirs {
+		os.MkdirAll(d, 0777)
+		time.Sleep(10 * time.Millisecond)
+	}
+	files := []string{
+		"testdata/src_dir/some_file",
+		"testdata/src_dir/some_",
+		"testdata/src_file",
+		"testdata/target_dir/some_file",
+		"testdata/target_file",
+	}
+	for _, v := range files {
+		err := ioutil.WriteFile(v, []byte(v), 0600)
+		if err != nil {
+			t.Fatal(err)
+		}
+		time.Sleep(500 * time.Millisecond)
+	}
+	defer func() {
+		err := os.RemoveAll("testdata")
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	table := []struct {
+		desc    string
+		src     string
+		targets []string
+		expect  bool
+	}{
+		{"Files only target checks", "testdata/src_file",
+			[]string{"testdata/target_file"}, true},
+		{"Files only target doesn't check", "testdata/target_file",
+			[]string{"testdata/src_file"}, false},
+		{"Directories only target checks", "testdata/src_dir",
+			[]string{"testdata/target_dir"}, true},
+		{"Directories only target doesn't  checks", "testdata/target_dir",
+			[]string{"testdata/src_dir"}, false},
+	}
+
+	for _, c := range table {
+		v, err := Path(c.src, c.targets...)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if v != c.expect {
+			t.Errorf("%s : expecting %v got %v", c.desc, c.expect, v)
+		}
+	}
+}