mirror of
https://github.com/go-task/task.git
synced 2026-06-29 23:55:18 +00:00
feat: add use_gitignore option to exclude ignored files from sources/generates (#2773)
This commit is contained in:
3
go.mod
3
go.mod
@@ -92,7 +92,7 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
@@ -121,6 +121,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -191,8 +191,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -284,8 +284,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
|
||||
211
internal/fingerprint/gitignore.go
Normal file
211
internal/fingerprint/gitignore.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/internal/gitignore"
|
||||
)
|
||||
|
||||
type linesCacheEntry struct {
|
||||
mtime time.Time
|
||||
lines []string
|
||||
}
|
||||
|
||||
var (
|
||||
gitignoreLinesCache sync.Map // dir -> linesCacheEntry, invalidated by mtime
|
||||
repoRootCache sync.Map // dir -> repo root (or "" when not in a repo)
|
||||
)
|
||||
|
||||
// findRepoRoot returns the first ancestor of dir containing a .git entry, or
|
||||
// ("", false) when dir is not inside a git repository.
|
||||
func findRepoRoot(dir string) (string, bool) {
|
||||
if v, ok := repoRootCache.Load(dir); ok {
|
||||
root := v.(string)
|
||||
return root, root != ""
|
||||
}
|
||||
|
||||
current := dir
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(current, ".git")); err == nil {
|
||||
repoRootCache.Store(dir, current)
|
||||
return current, true
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
break
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
|
||||
repoRootCache.Store(dir, "")
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filterGitignored marks entries matching gitignore rules as excluded (false).
|
||||
// All .gitignore files from the repo root down to each candidate file's
|
||||
// directory feed a single matcher so that precedence and cross-file negations
|
||||
// (`!pattern`) resolve correctly.
|
||||
func filterGitignored(files map[string]bool, dir string) map[string]bool {
|
||||
if len(files) == 0 {
|
||||
return files
|
||||
}
|
||||
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return files
|
||||
}
|
||||
repoRoot, ok := findRepoRoot(absDir)
|
||||
if !ok {
|
||||
return files
|
||||
}
|
||||
|
||||
// Every directory from the repo root down to each candidate file's dir, so
|
||||
// nested .gitignore files reached by deep globs are included too.
|
||||
dirSet := make(map[string]struct{})
|
||||
for path, included := range files {
|
||||
if !included {
|
||||
continue
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
d := filepath.Dir(absPath)
|
||||
if !withinRepo(repoRoot, d) {
|
||||
continue
|
||||
}
|
||||
for {
|
||||
dirSet[d] = struct{}{}
|
||||
if d == repoRoot {
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(d)
|
||||
if parent == d {
|
||||
break
|
||||
}
|
||||
d = parent
|
||||
}
|
||||
}
|
||||
|
||||
// Shallow dirs first (lower priority): the matcher scans patterns last to
|
||||
// first, so deeper rules win and can negate shallower ones.
|
||||
dirs := make([]string, 0, len(dirSet))
|
||||
for d := range dirSet {
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
sort.Slice(dirs, func(i, j int) bool {
|
||||
di := strings.Count(dirs[i], string(filepath.Separator))
|
||||
dj := strings.Count(dirs[j], string(filepath.Separator))
|
||||
if di != dj {
|
||||
return di < dj
|
||||
}
|
||||
return dirs[i] < dirs[j]
|
||||
})
|
||||
|
||||
var patterns []gitignore.Pattern
|
||||
for _, d := range dirs {
|
||||
lines := readGitignoreLines(d)
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
// domain scopes each pattern to its .gitignore subtree (go-git semantics).
|
||||
var domain []string
|
||||
if rel, err := filepath.Rel(repoRoot, d); err == nil && rel != "." {
|
||||
domain = strings.Split(filepath.ToSlash(rel), "/")
|
||||
}
|
||||
for _, line := range lines {
|
||||
patterns = append(patterns, gitignore.ParsePattern(line, domain))
|
||||
}
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
return files
|
||||
}
|
||||
|
||||
matcher := gitignore.NewMatcher(patterns)
|
||||
for path, included := range files {
|
||||
if !included {
|
||||
continue
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
relPath, err := filepath.Rel(repoRoot, absPath)
|
||||
if err != nil || relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
if ignored(matcher, strings.Split(filepath.ToSlash(relPath), "/")) {
|
||||
files[path] = false
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// ignored honors Git's rule that a file under an ignored directory cannot be
|
||||
// re-included by a deeper negation: if any ancestor directory is ignored, so is
|
||||
// the file. Otherwise the file's own verdict applies (isDir=false).
|
||||
func ignored(matcher gitignore.Matcher, segments []string) bool {
|
||||
for i := 1; i < len(segments); i++ {
|
||||
if matcher.Match(segments[:i], true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return matcher.Match(segments, false)
|
||||
}
|
||||
|
||||
func withinRepo(repoRoot, p string) bool {
|
||||
rel, err := filepath.Rel(repoRoot, p)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
||||
}
|
||||
|
||||
// readGitignoreLines returns the .gitignore lines in dir (nil if none), cached
|
||||
// per directory and invalidated by mtime so watch mode picks up edits.
|
||||
func readGitignoreLines(dir string) []string {
|
||||
path := filepath.Join(dir, ".gitignore")
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
mtime := info.ModTime()
|
||||
|
||||
if v, ok := gitignoreLinesCache.Load(dir); ok {
|
||||
entry := v.(linesCacheEntry)
|
||||
if entry.mtime.Equal(mtime) {
|
||||
return entry.lines
|
||||
}
|
||||
}
|
||||
|
||||
lines := parseGitignoreLines(path)
|
||||
gitignoreLinesCache.Store(dir, linesCacheEntry{mtime: mtime, lines: lines})
|
||||
return lines
|
||||
}
|
||||
|
||||
func parseGitignoreLines(path string) []string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimRight(scanner.Text(), "\r")
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
// On a scan error (e.g. an over-long line) keep what was parsed rather than
|
||||
// dropping the whole file.
|
||||
return lines
|
||||
}
|
||||
254
internal/fingerprint/gitignore_test.go
Normal file
254
internal/fingerprint/gitignore_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func initGitRepo(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
filesWithout, err := Globs(dir, globs, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
filesWith, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasLog := false
|
||||
for _, f := range filesWithout {
|
||||
if filepath.Base(f) == "ignored.log" {
|
||||
hasLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasLog, "ignored.log should be present without gitignore filter")
|
||||
|
||||
hasLog = false
|
||||
for _, f := range filesWith {
|
||||
if filepath.Base(f) == "ignored.log" {
|
||||
hasLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter")
|
||||
|
||||
txtCount := 0
|
||||
for _, f := range filesWith {
|
||||
if filepath.Ext(f) == ".txt" {
|
||||
txtCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, txtCount, "both .txt files should remain")
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreParentDirIgnored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
buildDir := filepath.Join(dir, "build")
|
||||
require.NoError(t, os.MkdirAll(buildDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(buildDir, "keep.txt"), []byte("keep"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(buildDir, "other.txt"), []byte("other"), 0o644))
|
||||
|
||||
// Git cannot re-include a file under an ignored directory.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("build/\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(buildDir, ".gitignore"), []byte("!keep.txt\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./**/*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
base := filepath.Base(f)
|
||||
assert.NotEqual(t, "keep.txt", base, "keep.txt must stay excluded under ignored build/")
|
||||
assert.NotEqual(t, "other.txt", base, "other.txt must stay excluded under ignored build/")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreNested(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
subDir := filepath.Join(dir, "sub")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(subDir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
assert.NotEqual(t, "build.out", filepath.Base(f), "build.out should be excluded by nested .gitignore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreCrossFileNegation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
subDir := filepath.Join(dir, "sub")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "debug.log"), []byte("debug"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "other.log"), []byte("other"), 0o644))
|
||||
|
||||
// Root ignores all *.log; a nested .gitignore re-includes debug.log.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("!debug.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(subDir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasDebug, hasOther := false, false
|
||||
for _, f := range files {
|
||||
switch filepath.Base(f) {
|
||||
case "debug.log":
|
||||
hasDebug = true
|
||||
case "other.log":
|
||||
hasOther = true
|
||||
}
|
||||
}
|
||||
assert.True(t, hasDebug, "debug.log should be re-included by the nested negation")
|
||||
assert.False(t, hasOther, "other.log should remain excluded by the root *.log rule")
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreDeepGlob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
subDir := filepath.Join(dir, "sub")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "gen.out"), []byte("gen"), 0o644))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./**/*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
assert.NotEqual(t, "gen.out", filepath.Base(f), "gen.out should be excluded by the nested .gitignore reached via deep glob")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreDoubleDotFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// A ".."-prefixed name must not be skipped by the out-of-tree guard.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "..keep.log"), []byte("x"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("y"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
assert.NotEqual(t, "..keep.log", filepath.Base(f), "..keep.log should be excluded by *.log")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreLongLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("x"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("y"), 0o644))
|
||||
|
||||
// A line over bufio.Scanner's 64KB limit triggers a scan error; patterns
|
||||
// parsed before it must survive.
|
||||
longLine := strings.Repeat("a", 70*1024)
|
||||
content := "*.log\n" + longLine + "\n"
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(content), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
assert.NotEqual(t, "ignored.log", filepath.Base(f), "*.log parsed before the long line should still apply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreNoRepo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Cannot use t.TempDir() here because it creates a dir inside the
|
||||
// go-task repo which has a .git parent, defeating the "no repo" test.
|
||||
dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") //nolint:usetesting
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, files, 1)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func Globs(dir string, globs []*ast.Glob) ([]string, error) {
|
||||
func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) {
|
||||
resultMap := make(map[string]bool)
|
||||
for _, g := range globs {
|
||||
matches, err := glob(dir, g.Glob)
|
||||
@@ -21,6 +21,11 @@ func Globs(dir string, globs []*ast.Glob) ([]string, error) {
|
||||
resultMap[match] = !g.Negate
|
||||
}
|
||||
}
|
||||
|
||||
if useGitignore {
|
||||
resultMap = filterGitignored(resultMap, dir)
|
||||
}
|
||||
|
||||
return collectKeys(resultMap), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func (*ChecksumChecker) Kind() string {
|
||||
}
|
||||
|
||||
func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) {
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
generates, err := Globs(t.Dir, t.Generates)
|
||||
generates, err := Globs(t.Dir, t.Generates, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (checker *TimestampChecker) Kind() string {
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (checker *TimestampChecker) Value(t *ast.Task) (any, error) {
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
201
internal/gitignore/LICENSE
Normal file
201
internal/gitignore/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Sourced Technologies, S.L.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
34
internal/gitignore/matcher.go
Normal file
34
internal/gitignore/matcher.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Vendored from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/matcher.go. Licensed under the Apache License 2.0;
|
||||
// see the LICENSE file in this directory.
|
||||
|
||||
package gitignore
|
||||
|
||||
// Matcher defines a global multi-pattern matcher for gitignore patterns
|
||||
type Matcher interface {
|
||||
// Match matches patterns in the order of priorities. As soon as an inclusion or
|
||||
// exclusion is found, not further matching is performed.
|
||||
Match(path []string, isDir bool) bool
|
||||
}
|
||||
|
||||
// NewMatcher constructs a new global matcher. Patterns must be given in the order of
|
||||
// increasing priority. That is most generic settings files first, then the content of
|
||||
// the repo .gitignore, then content of .gitignore down the path or the repo and then
|
||||
// the content command line arguments.
|
||||
func NewMatcher(ps []Pattern) Matcher {
|
||||
return &matcher{ps}
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
patterns []Pattern
|
||||
}
|
||||
|
||||
func (m *matcher) Match(path []string, isDir bool) bool {
|
||||
n := len(m.patterns)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if match := m.patterns[i].Match(path, isDir); match > NoMatch {
|
||||
return match == Exclude
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
23
internal/gitignore/matcher_test.go
Normal file
23
internal/gitignore/matcher_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/matcher_test.go. Licensed under the Apache
|
||||
// License 2.0; see LICENSE.
|
||||
|
||||
package gitignore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMatcher_Match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewMatcher([]Pattern{
|
||||
ParsePattern("**/middle/v[uo]l?ano", nil),
|
||||
ParsePattern("!volcano", nil),
|
||||
})
|
||||
|
||||
if got := m.Match([]string{"head", "middle", "vulkano"}, false); got != true {
|
||||
t.Errorf("Match(vulkano) = %t, want true", got)
|
||||
}
|
||||
if got := m.Match([]string{"head", "middle", "volcano"}, false); got != false {
|
||||
t.Errorf("Match(volcano) = %t, want false (negated)", got)
|
||||
}
|
||||
}
|
||||
165
internal/gitignore/pattern.go
Normal file
165
internal/gitignore/pattern.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Package gitignore implements gitignore pattern matching.
|
||||
//
|
||||
// This package is vendored from go-git:
|
||||
//
|
||||
// github.com/go-git/go-git/v5 v5.19.1, plumbing/format/gitignore
|
||||
//
|
||||
// Only the pattern parsing and matching logic (pattern.go and matcher.go) is
|
||||
// copied; the file-walking helpers (dir.go) are omitted as they pull in
|
||||
// go-billy and other go-git internals. The original code is licensed under the
|
||||
// Apache License 2.0; see the LICENSE file in this directory.
|
||||
package gitignore
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MatchResult defines outcomes of a match, no match, exclusion or inclusion.
|
||||
type MatchResult int
|
||||
|
||||
const (
|
||||
// NoMatch defines the no match outcome of a match check
|
||||
NoMatch MatchResult = iota
|
||||
// Exclude defines an exclusion of a file as a result of a match check
|
||||
Exclude
|
||||
// Include defines an explicit inclusion of a file as a result of a match check
|
||||
Include
|
||||
)
|
||||
|
||||
const (
|
||||
inclusionPrefix = "!"
|
||||
zeroToManyDirs = "**"
|
||||
patternDirSep = "/"
|
||||
)
|
||||
|
||||
// Pattern defines a single gitignore pattern.
|
||||
type Pattern interface {
|
||||
// Match matches the given path to the pattern.
|
||||
Match(path []string, isDir bool) MatchResult
|
||||
}
|
||||
|
||||
type pattern struct {
|
||||
domain []string
|
||||
pattern []string
|
||||
inclusion bool
|
||||
dirOnly bool
|
||||
isGlob bool
|
||||
}
|
||||
|
||||
// ParsePattern parses a gitignore pattern string into the Pattern structure.
|
||||
func ParsePattern(p string, domain []string) Pattern {
|
||||
// storing domain, copy it to ensure it isn't changed externally
|
||||
domain = append([]string(nil), domain...)
|
||||
res := pattern{domain: domain}
|
||||
|
||||
if strings.HasPrefix(p, inclusionPrefix) {
|
||||
res.inclusion = true
|
||||
p = p[1:]
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(p, "\\ ") {
|
||||
p = strings.TrimRight(p, " ")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(p, patternDirSep) {
|
||||
res.dirOnly = true
|
||||
p = p[:len(p)-1]
|
||||
}
|
||||
|
||||
if strings.Contains(p, patternDirSep) {
|
||||
res.isGlob = true
|
||||
}
|
||||
|
||||
res.pattern = strings.Split(p, patternDirSep)
|
||||
return &res
|
||||
}
|
||||
|
||||
func (p *pattern) Match(path []string, isDir bool) MatchResult {
|
||||
if len(path) <= len(p.domain) {
|
||||
return NoMatch
|
||||
}
|
||||
for i, e := range p.domain {
|
||||
if path[i] != e {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
path = path[len(p.domain):]
|
||||
if p.isGlob && !p.globMatch(path, isDir) {
|
||||
return NoMatch
|
||||
} else if !p.isGlob && !p.simpleNameMatch(path, isDir) {
|
||||
return NoMatch
|
||||
}
|
||||
|
||||
if p.inclusion {
|
||||
return Include
|
||||
} else {
|
||||
return Exclude
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pattern) simpleNameMatch(path []string, isDir bool) bool {
|
||||
for i, name := range path {
|
||||
if match, err := filepath.Match(p.pattern[0], name); err != nil {
|
||||
return false
|
||||
} else if !match {
|
||||
continue
|
||||
}
|
||||
if p.dirOnly && !isDir && i == len(path)-1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *pattern) globMatch(path []string, isDir bool) bool {
|
||||
matched := false
|
||||
canTraverse := false
|
||||
for i, pattern := range p.pattern {
|
||||
if pattern == "" {
|
||||
canTraverse = false
|
||||
continue
|
||||
}
|
||||
if pattern == zeroToManyDirs {
|
||||
if i == len(p.pattern)-1 {
|
||||
break
|
||||
}
|
||||
canTraverse = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(pattern, zeroToManyDirs) {
|
||||
return false
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return false
|
||||
}
|
||||
if canTraverse {
|
||||
canTraverse = false
|
||||
for len(path) > 0 {
|
||||
e := path[0]
|
||||
path = path[1:]
|
||||
if match, err := filepath.Match(pattern, e); err != nil {
|
||||
return false
|
||||
} else if match {
|
||||
matched = true
|
||||
break
|
||||
} else if len(path) == 0 {
|
||||
// if nothing left then fail
|
||||
matched = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
|
||||
return false
|
||||
}
|
||||
matched = true
|
||||
path = path[1:]
|
||||
}
|
||||
}
|
||||
if matched && p.dirOnly && !isDir && len(path) == 0 {
|
||||
matched = false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
80
internal/gitignore/pattern_test.go
Normal file
80
internal/gitignore/pattern_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/pattern_test.go (originally written against
|
||||
// gopkg.in/check.v1; rewritten here as table-driven stdlib tests to avoid an
|
||||
// extra test dependency). Licensed under the Apache License 2.0; see LICENSE.
|
||||
|
||||
package gitignore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParsePattern_Match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
domain []string
|
||||
path []string
|
||||
isDir bool
|
||||
want MatchResult
|
||||
}{
|
||||
{"inclusion", "!vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Include},
|
||||
{"domainLonger_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle"}, false, NoMatch},
|
||||
{"domainSameLength_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "tail"}, false, NoMatch},
|
||||
{"domainMismatch_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "_tail_", "value"}, false, NoMatch},
|
||||
{"withDomain", "middle/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, false, Exclude},
|
||||
{"onlyMatchInDomain_mismatch", "volcano/", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, true, NoMatch},
|
||||
{"atStart", "value", nil, []string{"value", "tail"}, false, Exclude},
|
||||
{"inTheMiddle", "value", nil, []string{"head", "value", "tail"}, false, Exclude},
|
||||
{"atEnd", "value", nil, []string{"head", "value"}, false, Exclude},
|
||||
{"atStart_dirWanted", "value/", nil, []string{"value", "tail"}, false, Exclude},
|
||||
{"inTheMiddle_dirWanted", "value/", nil, []string{"head", "value", "tail"}, false, Exclude},
|
||||
{"atEnd_dirWanted", "value/", nil, []string{"head", "value"}, true, Exclude},
|
||||
{"atEnd_dirWanted_notADir_mismatch", "value/", nil, []string{"head", "value"}, false, NoMatch},
|
||||
{"mismatch", "value", nil, []string{"head", "val", "tail"}, false, NoMatch},
|
||||
{"valueLonger_mismatch", "val", nil, []string{"head", "value", "tail"}, false, NoMatch},
|
||||
{"withAsterisk", "v*o", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"withQuestionMark", "vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"magicChars", "v[ou]l[kc]ano", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"wrongPattern_mismatch", "v[ou]l[", nil, []string{"value", "vol["}, false, NoMatch},
|
||||
{"glob_fromRootWithSlash", "/value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"glob_withDomain", "middle/tail/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, true, Exclude},
|
||||
{"glob_onlyMatchInDomain_mismatch", "volcano/tail", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, false, NoMatch},
|
||||
{"glob_fromRootWithoutSlash", "value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"glob_fromRoot_mismatch", "value/vulkano", nil, []string{"value", "volcano"}, false, NoMatch},
|
||||
{"glob_fromRoot_tooShort_mismatch", "value/vul?ano", nil, []string{"value"}, false, NoMatch},
|
||||
{"glob_fromRoot_notAtRoot_mismatch", "/value/volcano", nil, []string{"value", "value", "volcano"}, false, NoMatch},
|
||||
{"glob_leadingAsterisks_atStart", "**/*lue/vol?ano", nil, []string{"value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_notAtStart", "**/*lue/vol?ano", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_mismatch", "**/*lue/vol?ano", nil, []string{"head", "value", "Volcano", "tail"}, false, NoMatch},
|
||||
{"glob_leadingAsterisks_isDir", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_isDirAtEnd", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, true, Exclude},
|
||||
{"glob_leadingAsterisks_isDir_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "Colcano"}, true, NoMatch},
|
||||
{"glob_leadingAsterisks_isDirNoDirAtEnd_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, false, NoMatch},
|
||||
{"glob_tailingAsterisks", "/*lue/vol?ano/**", nil, []string{"value", "volcano", "tail", "moretail"}, false, Exclude},
|
||||
{"glob_tailingAsterisks_exactMatch", "/*lue/vol?ano/**", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_emptyMatch", "/*lue/**/vol?ano", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_oneMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_multiMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle1", "middle2", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_isDir_trailing", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, true, Exclude},
|
||||
{"glob_middleAsterisks_isDir_trailing_mismatch", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, false, NoMatch},
|
||||
{"glob_middleAsterisks_isDir", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_wrongDoubleAsterisk_mismatch", "/*lue/**foo/vol?ano", nil, []string{"value", "foo", "volcano", "tail"}, false, NoMatch},
|
||||
{"glob_magicChars", "**/head/v[ou]l[kc]ano", nil, []string{"value", "head", "volcano"}, false, Exclude},
|
||||
{"glob_wrongPattern_noTraversal_mismatch", "**/head/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
|
||||
{"glob_wrongPattern_onTraversal_mismatch", "/value/**/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
|
||||
{"glob_issue_923", "**/android/**/GeneratedPluginRegistrant.java", nil, []string{"packages", "flutter_tools", "lib", "src", "android", "gradle.dart"}, false, NoMatch},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := ParsePattern(tt.pattern, tt.domain)
|
||||
if got := p.Match(tt.path, tt.isDir); got != tt.want {
|
||||
t.Errorf("ParsePattern(%q, %v).Match(%v, %t) = %v, want %v",
|
||||
tt.pattern, tt.domain, tt.path, tt.isDir, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
158
task_test.go
158
task_test.go
@@ -653,6 +653,164 @@ func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest /
|
||||
require.NoError(t, err, "generated.txt should be recreated after third run")
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, dir, name, content string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, name), []byte(content), 0o644))
|
||||
}
|
||||
|
||||
// gitignoreStep writes a set of files then runs the task once, capturing its
|
||||
// output as a golden fixture named run.
|
||||
type gitignoreStep struct {
|
||||
write map[string]string
|
||||
run string
|
||||
}
|
||||
|
||||
// gitignoreSeq drives a checksum task through a sequence of runs against a
|
||||
// fixture dir. create seeds runtime files (removed on cleanup); restore resets
|
||||
// tracked files to their committed content on cleanup; artifacts are
|
||||
// task-produced files to delete on cleanup.
|
||||
type gitignoreSeq struct {
|
||||
dir string
|
||||
task string
|
||||
create map[string]string
|
||||
restore map[string]string
|
||||
artifacts []string
|
||||
steps []gitignoreStep
|
||||
}
|
||||
|
||||
func (s gitignoreSeq) run(t *testing.T) {
|
||||
t.Helper()
|
||||
cleanup := func() {
|
||||
_ = os.RemoveAll(filepathext.SmartJoin(s.dir, ".task"))
|
||||
for name := range s.create {
|
||||
_ = os.Remove(filepathext.SmartJoin(s.dir, name))
|
||||
}
|
||||
for _, name := range s.artifacts {
|
||||
_ = os.Remove(filepathext.SmartJoin(s.dir, name))
|
||||
}
|
||||
for name, content := range s.restore {
|
||||
writeFile(t, s.dir, name, content)
|
||||
}
|
||||
}
|
||||
cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
for name, content := range s.create {
|
||||
writeFile(t, s.dir, name, content)
|
||||
}
|
||||
for _, step := range s.steps {
|
||||
for name, content := range step.write {
|
||||
writeFile(t, s.dir, name, content)
|
||||
}
|
||||
NewExecutorTest(t,
|
||||
WithName(step.run),
|
||||
WithExecutorOptions(task.WithDir(s.dir)),
|
||||
WithTask(s.task),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreChecksum(t *testing.T) { //nolint:paralleltest // shares testdata/gitignore and mutates fixture files
|
||||
gitignoreSeq{
|
||||
dir: "testdata/gitignore",
|
||||
task: "build",
|
||||
create: map[string]string{"ignored.txt": "ignored\n"},
|
||||
restore: map[string]string{"source.txt": "source content\n"},
|
||||
artifacts: []string{"generated.txt"},
|
||||
steps: []gitignoreStep{
|
||||
{run: "first run"},
|
||||
{run: "up to date"},
|
||||
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
|
||||
{run: "source file modified", write: map[string]string{"source.txt": "source modified\n"}},
|
||||
},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
// TestGitignoreNegation checks that a `!pattern` in a nested .gitignore
|
||||
// re-includes a file excluded by a parent .gitignore.
|
||||
func TestGitignoreNegation(t *testing.T) { //nolint:paralleltest // mutates fixture files
|
||||
gitignoreSeq{
|
||||
dir: "testdata/gitignore_negation",
|
||||
task: "build",
|
||||
create: map[string]string{"sub/debug.log": "debug\n", "sub/other.log": "other\n"},
|
||||
steps: []gitignoreStep{
|
||||
{run: "first run"},
|
||||
{run: "up to date"},
|
||||
{run: "ignored file modified", write: map[string]string{"sub/other.log": "other modified\n"}},
|
||||
{run: "reincluded file modified", write: map[string]string{"sub/debug.log": "debug modified\n"}},
|
||||
},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
// TestGitignoreNested checks that a .gitignore in a subdirectory below the task
|
||||
// dir is honored when its files are reached by a deep glob.
|
||||
func TestGitignoreNested(t *testing.T) { //nolint:paralleltest // mutates fixture files
|
||||
gitignoreSeq{
|
||||
dir: "testdata/gitignore_nested",
|
||||
task: "build",
|
||||
create: map[string]string{"sub/secret.dat": "secret\n"},
|
||||
restore: map[string]string{"sub/keep.txt": "keep\n"},
|
||||
steps: []gitignoreStep{
|
||||
{run: "first run"},
|
||||
{run: "up to date"},
|
||||
{run: "ignored file modified", write: map[string]string{"sub/secret.dat": "secret modified\n"}},
|
||||
{run: "source file modified", write: map[string]string{"sub/keep.txt": "keep modified\n"}},
|
||||
},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
// TestGitignoreIncluded checks that a top-level use_gitignore in an included
|
||||
// Taskfile is propagated onto its tasks during merge.
|
||||
func TestGitignoreIncluded(t *testing.T) { //nolint:paralleltest // mutates fixture files
|
||||
gitignoreSeq{
|
||||
dir: "testdata/gitignore_included",
|
||||
task: "included:build",
|
||||
create: map[string]string{"ignored.txt": "ignored\n"},
|
||||
steps: []gitignoreStep{
|
||||
{run: "first run"},
|
||||
{run: "up to date"},
|
||||
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
|
||||
},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
// TestGitignoreIncludedOverride checks that an explicit use_gitignore: false in
|
||||
// an included Taskfile is preserved even when the root Taskfile sets it to true.
|
||||
func TestGitignoreIncludedOverride(t *testing.T) { //nolint:paralleltest // mutates fixture files
|
||||
gitignoreSeq{
|
||||
dir: "testdata/gitignore_included_override",
|
||||
task: "included:build",
|
||||
create: map[string]string{"ignored.txt": "ignored\n"},
|
||||
steps: []gitignoreStep{
|
||||
{run: "first run"},
|
||||
{run: "up to date"},
|
||||
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
|
||||
},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestGitignoreTaskListFallback(t *testing.T) { //nolint:paralleltest // shares testdata/gitignore with TestGitignoreChecksum
|
||||
const dir = "testdata/gitignore"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
listed, err := e.CompiledTaskForTaskList(&task.Call{Task: "build"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, listed.ShouldUseGitignore(),
|
||||
"task list should reflect the global use_gitignore fallback")
|
||||
|
||||
// "build-no-use_gitignore" explicitly disables it.
|
||||
listedOff, err := e.CompiledTaskForTaskList(&task.Call{Task: "build-no-use_gitignore"})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, listedOff.ShouldUseGitignore(),
|
||||
"explicit use_gitignore: false must be preserved in the list path")
|
||||
}
|
||||
|
||||
func TestStatusVariables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type Task struct {
|
||||
Method string
|
||||
Prefix string `hash:"ignore"`
|
||||
IgnoreError bool
|
||||
UseGitignore *bool
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
@@ -75,6 +76,12 @@ func (t *Task) IsSilent() bool {
|
||||
return t.Silent != nil && *t.Silent
|
||||
}
|
||||
|
||||
// ShouldUseGitignore returns true if gitignore filtering is enabled for the task.
|
||||
// Returns false if UseGitignore is nil or set to false.
|
||||
func (t *Task) ShouldUseGitignore() bool {
|
||||
return t.UseGitignore != nil && *t.UseGitignore
|
||||
}
|
||||
|
||||
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
|
||||
func (t *Task) WildcardMatch(name string) (bool, []string) {
|
||||
names := append([]string{t.Task}, t.Aliases...)
|
||||
@@ -149,7 +156,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
UseGitignore *bool `yaml:"use_gitignore,omitempty"`
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
@@ -190,6 +198,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
t.Method = task.Method
|
||||
t.Prefix = task.Prefix
|
||||
t.IgnoreError = task.IgnoreError
|
||||
t.UseGitignore = deepcopy.Scalar(task.UseGitignore)
|
||||
t.Run = task.Run
|
||||
t.Platforms = task.Platforms
|
||||
t.If = task.If
|
||||
@@ -233,6 +242,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
Method: t.Method,
|
||||
Prefix: t.Prefix,
|
||||
IgnoreError: t.IgnoreError,
|
||||
UseGitignore: deepcopy.Scalar(t.UseGitignore),
|
||||
Run: t.Run,
|
||||
IncludeVars: t.IncludeVars.DeepCopy(),
|
||||
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
|
||||
|
||||
@@ -20,20 +20,21 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c
|
||||
|
||||
// Taskfile is the abstract syntax tree for a Taskfile
|
||||
type Taskfile struct {
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
UseGitignore *bool
|
||||
}
|
||||
|
||||
// Merge merges the second Taskfile into the first
|
||||
@@ -67,6 +68,14 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if t2.UseGitignore != nil {
|
||||
for _, t := range t2.Tasks.All(nil) {
|
||||
if t.UseGitignore == nil {
|
||||
v := *t2.UseGitignore
|
||||
t.UseGitignore = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
@@ -76,19 +85,20 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
var taskfile struct {
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
UseGitignore *bool `yaml:"use_gitignore"`
|
||||
}
|
||||
if err := node.Decode(&taskfile); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -106,6 +116,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
tf.Run = taskfile.Run
|
||||
tf.Interval = taskfile.Interval
|
||||
tf.UseGitignore = taskfile.UseGitignore
|
||||
if tf.Includes == nil {
|
||||
tf.Includes = NewIncludes()
|
||||
}
|
||||
|
||||
1
testdata/gitignore/.gitignore
vendored
Normal file
1
testdata/gitignore/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ignored.txt
|
||||
25
testdata/gitignore/Taskfile.yml
vendored
Normal file
25
testdata/gitignore/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./*.txt
|
||||
- exclude: ./generated.txt
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
|
||||
build-no-use_gitignore:
|
||||
use_gitignore: false
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./*.txt
|
||||
- exclude: ./generated.txt
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
1
testdata/gitignore/source.txt
vendored
Normal file
1
testdata/gitignore/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
source content
|
||||
1
testdata/gitignore/testdata/TestGitignoreChecksum-first_run.golden
vendored
Normal file
1
testdata/gitignore/testdata/TestGitignoreChecksum-first_run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: [build] cp ./source.txt ./generated.txt
|
||||
1
testdata/gitignore/testdata/TestGitignoreChecksum-ignored_file_modified.golden
vendored
Normal file
1
testdata/gitignore/testdata/TestGitignoreChecksum-ignored_file_modified.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
1
testdata/gitignore/testdata/TestGitignoreChecksum-source_file_modified.golden
vendored
Normal file
1
testdata/gitignore/testdata/TestGitignoreChecksum-source_file_modified.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: [build] cp ./source.txt ./generated.txt
|
||||
1
testdata/gitignore/testdata/TestGitignoreChecksum-up_to_date.golden
vendored
Normal file
1
testdata/gitignore/testdata/TestGitignoreChecksum-up_to_date.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
1
testdata/gitignore_included/.gitignore
vendored
Normal file
1
testdata/gitignore_included/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ignored.txt
|
||||
9
testdata/gitignore_included/Taskfile.yml
vendored
Normal file
9
testdata/gitignore_included/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included: ./included/Taskfile.yml
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "root"
|
||||
11
testdata/gitignore_included/included/Taskfile.yml
vendored
Normal file
11
testdata/gitignore_included/included/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "build executed"
|
||||
sources:
|
||||
- ./*.txt
|
||||
method: checksum
|
||||
1
testdata/gitignore_included/source.txt
vendored
Normal file
1
testdata/gitignore_included/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
source
|
||||
2
testdata/gitignore_included/testdata/TestGitignoreIncluded-first_run.golden
vendored
Normal file
2
testdata/gitignore_included/testdata/TestGitignoreIncluded-first_run.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [included:build] echo "build executed"
|
||||
build executed
|
||||
1
testdata/gitignore_included/testdata/TestGitignoreIncluded-ignored_file_modified.golden
vendored
Normal file
1
testdata/gitignore_included/testdata/TestGitignoreIncluded-ignored_file_modified.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "included:build" is up to date
|
||||
1
testdata/gitignore_included/testdata/TestGitignoreIncluded-up_to_date.golden
vendored
Normal file
1
testdata/gitignore_included/testdata/TestGitignoreIncluded-up_to_date.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "included:build" is up to date
|
||||
1
testdata/gitignore_included_override/.gitignore
vendored
Normal file
1
testdata/gitignore_included_override/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ignored.txt
|
||||
11
testdata/gitignore_included_override/Taskfile.yml
vendored
Normal file
11
testdata/gitignore_included_override/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
includes:
|
||||
included: ./included/Taskfile.yml
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "root"
|
||||
11
testdata/gitignore_included_override/included/Taskfile.yml
vendored
Normal file
11
testdata/gitignore_included_override/included/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: false
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "build executed"
|
||||
sources:
|
||||
- ./*.txt
|
||||
method: checksum
|
||||
1
testdata/gitignore_included_override/source.txt
vendored
Normal file
1
testdata/gitignore_included_override/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
source
|
||||
2
testdata/gitignore_included_override/testdata/TestGitignoreIncludedOverride-first_run.golden
vendored
Normal file
2
testdata/gitignore_included_override/testdata/TestGitignoreIncludedOverride-first_run.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [included:build] echo "build executed"
|
||||
build executed
|
||||
@@ -0,0 +1,2 @@
|
||||
task: [included:build] echo "build executed"
|
||||
build executed
|
||||
@@ -0,0 +1 @@
|
||||
task: Task "included:build" is up to date
|
||||
1
testdata/gitignore_negation/.gitignore
vendored
Normal file
1
testdata/gitignore_negation/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log
|
||||
11
testdata/gitignore_negation/Taskfile.yml
vendored
Normal file
11
testdata/gitignore_negation/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "build executed"
|
||||
sources:
|
||||
- ./sub/*.log
|
||||
method: checksum
|
||||
1
testdata/gitignore_negation/sub/.gitignore
vendored
Normal file
1
testdata/gitignore_negation/sub/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!debug.log
|
||||
2
testdata/gitignore_negation/testdata/TestGitignoreNegation-first_run.golden
vendored
Normal file
2
testdata/gitignore_negation/testdata/TestGitignoreNegation-first_run.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [build] echo "build executed"
|
||||
build executed
|
||||
1
testdata/gitignore_negation/testdata/TestGitignoreNegation-ignored_file_modified.golden
vendored
Normal file
1
testdata/gitignore_negation/testdata/TestGitignoreNegation-ignored_file_modified.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
2
testdata/gitignore_negation/testdata/TestGitignoreNegation-reincluded_file_modified.golden
vendored
Normal file
2
testdata/gitignore_negation/testdata/TestGitignoreNegation-reincluded_file_modified.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [build] echo "build executed"
|
||||
build executed
|
||||
1
testdata/gitignore_negation/testdata/TestGitignoreNegation-up_to_date.golden
vendored
Normal file
1
testdata/gitignore_negation/testdata/TestGitignoreNegation-up_to_date.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
11
testdata/gitignore_nested/Taskfile.yml
vendored
Normal file
11
testdata/gitignore_nested/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "build executed"
|
||||
sources:
|
||||
- ./sub/*
|
||||
method: checksum
|
||||
1
testdata/gitignore_nested/sub/.gitignore
vendored
Normal file
1
testdata/gitignore_nested/sub/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
secret.dat
|
||||
1
testdata/gitignore_nested/sub/keep.txt
vendored
Normal file
1
testdata/gitignore_nested/sub/keep.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
keep
|
||||
2
testdata/gitignore_nested/testdata/TestGitignoreNested-first_run.golden
vendored
Normal file
2
testdata/gitignore_nested/testdata/TestGitignoreNested-first_run.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [build] echo "build executed"
|
||||
build executed
|
||||
1
testdata/gitignore_nested/testdata/TestGitignoreNested-ignored_file_modified.golden
vendored
Normal file
1
testdata/gitignore_nested/testdata/TestGitignoreNested-ignored_file_modified.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
2
testdata/gitignore_nested/testdata/TestGitignoreNested-source_file_modified.golden
vendored
Normal file
2
testdata/gitignore_nested/testdata/TestGitignoreNested-source_file_modified.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: [build] echo "build executed"
|
||||
build executed
|
||||
1
testdata/gitignore_nested/testdata/TestGitignoreNested-up_to_date.golden
vendored
Normal file
1
testdata/gitignore_nested/testdata/TestGitignoreNested-up_to_date.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Task "build" is up to date
|
||||
25
variables.go
25
variables.go
@@ -19,6 +19,16 @@ import (
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
// shouldTaskUseGitignore resolves whether gitignore filtering applies to a
|
||||
// task: the task-level value takes precedence, falling back to the Taskfile's
|
||||
// global use_gitignore when the task does not set it.
|
||||
func (e *Executor) shouldTaskUseGitignore(t *ast.Task) bool {
|
||||
if t.UseGitignore != nil {
|
||||
return *t.UseGitignore
|
||||
}
|
||||
return e.Taskfile.UseGitignore != nil && *e.Taskfile.UseGitignore
|
||||
}
|
||||
|
||||
// CompiledTask returns a copy of a task, but replacing variables in almost all
|
||||
// properties using the Go template package.
|
||||
func (e *Executor) CompiledTask(call *Call) (*ast.Task, error) {
|
||||
@@ -43,6 +53,8 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
|
||||
|
||||
cache := &templater.Cache{Vars: vars}
|
||||
|
||||
gitignore := e.shouldTaskUseGitignore(origTask)
|
||||
|
||||
return &ast.Task{
|
||||
Task: origTask.Task,
|
||||
Label: templater.Replace(origTask.Label, cache),
|
||||
@@ -59,6 +71,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
|
||||
Env: nil,
|
||||
Dotenv: origTask.Dotenv,
|
||||
Silent: deepcopy.Scalar(origTask.Silent),
|
||||
UseGitignore: &gitignore,
|
||||
Interactive: origTask.Interactive,
|
||||
Internal: origTask.Internal,
|
||||
Method: origTask.Method,
|
||||
@@ -110,6 +123,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
}
|
||||
}
|
||||
|
||||
gitignore := e.shouldTaskUseGitignore(origTask)
|
||||
|
||||
new := ast.Task{
|
||||
Task: origTask.Task,
|
||||
Label: templater.Replace(origTask.Label, cache),
|
||||
@@ -126,6 +141,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
Env: nil,
|
||||
Dotenv: templater.Replace(origTask.Dotenv, cache),
|
||||
Silent: deepcopy.Scalar(origTask.Silent),
|
||||
UseGitignore: &gitignore,
|
||||
Interactive: origTask.Interactive,
|
||||
Internal: origTask.Internal,
|
||||
Method: templater.Replace(origTask.Method, cache),
|
||||
@@ -219,7 +235,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
continue
|
||||
}
|
||||
if cmd.For != nil {
|
||||
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
|
||||
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -272,7 +288,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
continue
|
||||
}
|
||||
if dep.For != nil {
|
||||
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
|
||||
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -343,6 +359,7 @@ func itemsFromFor(
|
||||
dir string,
|
||||
sources []*ast.Glob,
|
||||
generates []*ast.Glob,
|
||||
gitignore bool,
|
||||
vars *ast.Vars,
|
||||
location *ast.Location,
|
||||
cache *templater.Cache,
|
||||
@@ -366,7 +383,7 @@ func itemsFromFor(
|
||||
}
|
||||
// Get the list from the task sources
|
||||
if f.From == "sources" {
|
||||
glist, err := fingerprint.Globs(dir, sources)
|
||||
glist, err := fingerprint.Globs(dir, sources, gitignore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -380,7 +397,7 @@ func itemsFromFor(
|
||||
}
|
||||
// Get the list from the task generates
|
||||
if f.From == "generates" {
|
||||
glist, err := fingerprint.Globs(dir, generates)
|
||||
glist, err := fingerprint.Globs(dir, generates, gitignore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
2
watch.go
2
watch.go
@@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) {
|
||||
var sources []string
|
||||
|
||||
err := e.traverse(calls, func(task *ast.Task) error {
|
||||
files, err := fingerprint.Globs(task.Dir, task.Sources)
|
||||
files, err := fingerprint.Globs(task.Dir, task.Sources, task.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -177,6 +177,11 @@
|
||||
"enum": ["none", "checksum", "timestamp"],
|
||||
"default": "none"
|
||||
},
|
||||
"use_gitignore": {
|
||||
"description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution. Overrides the global gitignore setting.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"prefix": {
|
||||
"description": "Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`.",
|
||||
"type": "string"
|
||||
@@ -694,6 +699,11 @@
|
||||
"enum": ["none", "checksum", "timestamp"],
|
||||
"default": "checksum"
|
||||
},
|
||||
"use_gitignore": {
|
||||
"description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution for all tasks. Can be overridden per task.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"includes": {
|
||||
"description": "Imports tasks from the specified taskfiles. The tasks described in the given Taskfiles will be available with the informed namespace.",
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user