Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/fingerprint/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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/fsext"
"github.com/go-task/task/v3/taskfile/ast"
)

Expand All @@ -32,6 +33,10 @@ func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) {
func glob(dir string, g string) ([]string, error) {
g = filepathext.SmartJoin(dir, g)

if results, ok, err := fsext.FastRecursiveGlob(g); ok {
return results, err
}

fs, err := execext.ExpandFields(g)
if err != nil {
return nil, err
Expand Down
79 changes: 79 additions & 0 deletions internal/fsext/glob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package fsext

import (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"

"github.com/go-task/task/v3/errors"
)

var errFastGlobFallback = errors.New("fast glob fallback")

// FastRecursiveGlob expands simple literal-root recursive patterns. The
// boolean reports whether the pattern was handled or requires the full shell
// glob expander.
func FastRecursiveGlob(pattern string) ([]string, bool, error) {
pattern = filepath.Clean(pattern)
separator := string(os.PathSeparator)
marker := separator + "**" + separator

idx := strings.Index(pattern, marker)
if idx == -1 || strings.Contains(pattern[idx+len(marker):], marker) {
return nil, false, nil
}

root := pattern[:idx]
namePattern := pattern[idx+len(marker):]
if root == "" || namePattern == "" || strings.Contains(namePattern, separator) {
return nil, false, nil
}
if strings.Contains(root, "**") || strings.ContainsAny(root, "*?[]{}") {
return nil, false, nil
}
if strings.ContainsAny(namePattern, "{}") {
return nil, false, nil
}

var results []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if path == root {
return errFastGlobFallback
}

if d.Type()&fs.ModeSymlink != 0 {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
return errFastGlobFallback
}
}

matched, err := filepath.Match(namePattern, d.Name())
if err != nil {
return err
}
if matched {
results = append(results, filepath.ToSlash(path))
}
return nil
})
if errors.Is(err, errFastGlobFallback) {
return nil, false, nil
}
if err != nil {
return nil, true, err
}
sort.Strings(results)
return results, true, nil
}
92 changes: 92 additions & 0 deletions internal/fsext/glob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package fsext

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestFastRecursiveGlob(t *testing.T) {
t.Parallel()

root := filepath.Join(t.TempDir(), "root")
require.NoError(t, os.MkdirAll(filepath.Join(root, "nested", "deeper"), 0o755))

files := []string{
filepath.Join(root, "direct.yaml"),
filepath.Join(root, "nested", "alpha.yaml"),
filepath.Join(root, "nested", "deeper", "beta.yaml"),
filepath.Join(root, "nested", "ignored.txt"),
}
for _, file := range files {
require.NoError(t, os.WriteFile(file, nil, 0o600))
}

got, ok, err := FastRecursiveGlob(filepath.Join(root, "**", "*.yaml"))
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, []string{
filepath.ToSlash(files[0]),
filepath.ToSlash(files[1]),
filepath.ToSlash(files[2]),
}, got)
}

func TestFastRecursiveGlobFallback(t *testing.T) {
t.Parallel()

root := filepath.Join(t.TempDir(), "root")
tests := []struct {
name string
pattern string
}{
{name: "no recursive marker", pattern: filepath.Join(root, "*.yaml")},
{name: "wildcard root", pattern: filepath.Join(root, "*", "**", "*.yaml")},
{name: "nested suffix", pattern: filepath.Join(root, "**", "generated", "*.yaml")},
{name: "brace suffix", pattern: filepath.Join(root, "**", "*.{yaml,yml}")},
{name: "multiple recursive markers", pattern: filepath.Join(root, "**", "nested", "**", "*.yaml")},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, ok, err := FastRecursiveGlob(tt.pattern)
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, got)
})
}
}

func TestFastRecursiveGlobNonDirectoryRoot(t *testing.T) {
t.Parallel()

root := filepath.Join(t.TempDir(), "root.yaml")
require.NoError(t, os.WriteFile(root, nil, 0o600))

got, ok, err := FastRecursiveGlob(filepath.Join(root, "**", "*.yaml"))
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, got)
}

func TestFastRecursiveGlobSymlinkDirectoryFallback(t *testing.T) {
t.Parallel()

tempDir := t.TempDir()
root := filepath.Join(tempDir, "root")
target := filepath.Join(tempDir, "target")
require.NoError(t, os.MkdirAll(root, 0o755))
require.NoError(t, os.MkdirAll(target, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(target, "file.yaml"), nil, 0o600))
if err := os.Symlink(target, filepath.Join(root, "linked")); err != nil {
t.Skipf("cannot create directory symlink: %v", err)
}

got, ok, err := FastRecursiveGlob(filepath.Join(root, "**", "*.yaml"))
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, got)
}
Loading