mirror of
https://github.com/go-task/task.git
synced 2026-07-09 06:55:14 +00:00
feat: add gitignore option to exclude ignored files from sources/generates
When `gitignore: true` is set at the Taskfile or task level, files matching .gitignore rules are automatically excluded from sources and generates glob resolution. This prevents rebuilds triggered by changes to files that are in .gitignore (build artifacts, generated files, etc.). Uses go-git to load .gitignore patterns including nested .gitignore files, .git/info/exclude, and global gitignore configuration.
This commit is contained in:
60
internal/fingerprint/gitignore.go
Normal file
60
internal/fingerprint/gitignore.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
)
|
||||
|
||||
// filterGitignored removes entries from the file map that match gitignore rules.
|
||||
// Files are expected to be absolute paths. The dir parameter is used to find the git repository.
|
||||
// Returns the input map unchanged if the directory is not inside a git repository.
|
||||
func filterGitignored(files map[string]bool, dir string) map[string]bool {
|
||||
repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{
|
||||
DetectDotGit: true,
|
||||
})
|
||||
if err != nil {
|
||||
return files
|
||||
}
|
||||
|
||||
wt, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return files
|
||||
}
|
||||
|
||||
var allPatterns []gitignore.Pattern
|
||||
|
||||
if ps, err := gitignore.LoadSystemPatterns(wt.Filesystem); err == nil {
|
||||
allPatterns = append(allPatterns, ps...)
|
||||
}
|
||||
|
||||
if ps, err := gitignore.LoadGlobalPatterns(wt.Filesystem); err == nil {
|
||||
allPatterns = append(allPatterns, ps...)
|
||||
}
|
||||
|
||||
if ps, err := gitignore.ReadPatterns(wt.Filesystem, nil); err == nil {
|
||||
allPatterns = append(allPatterns, ps...)
|
||||
}
|
||||
|
||||
if len(allPatterns) == 0 {
|
||||
return files
|
||||
}
|
||||
|
||||
matcher := gitignore.NewMatcher(allPatterns)
|
||||
gitRoot := wt.Filesystem.Root()
|
||||
|
||||
for path := range files {
|
||||
relPath, err := filepath.Rel(gitRoot, path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pathComponents := strings.Split(filepath.ToSlash(relPath), "/")
|
||||
if matcher.Match(pathComponents, false) {
|
||||
files[path] = false
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
127
internal/fingerprint/gitignore_test.go
Normal file
127
internal/fingerprint/gitignore_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"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()
|
||||
_, err := git.PlainInit(dir, false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, err := os.MkdirTemp("", "task-gitignore-test-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// Create test files
|
||||
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))
|
||||
|
||||
// Create .gitignore
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
// Without gitignore - should include all files
|
||||
filesWithout, err := Globs(dir, globs, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// With gitignore - should exclude .log files
|
||||
filesWith, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The .log file should be in the unfiltered list
|
||||
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")
|
||||
|
||||
// The .log file should NOT be in the filtered list
|
||||
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")
|
||||
|
||||
// .txt files should still be present
|
||||
txtCount := 0
|
||||
for _, f := range filesWith {
|
||||
if filepath.Ext(f) == ".txt" {
|
||||
txtCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, txtCount, "both .txt files should remain")
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, err := os.MkdirTemp("", "task-gitignore-disabled-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
|
||||
initGitRepo(t, dir)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.log"), []byte("content"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
// WithGitignore(false, ...) should not filter
|
||||
files, err := Globs(dir, globs, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasLog := false
|
||||
for _, f := range files {
|
||||
if filepath.Base(f) == "file.log" {
|
||||
hasLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasLog, "file.log should be present when gitignore is disabled")
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreNoRepo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, err := os.MkdirTemp("", "task-gitignore-norepo-*")
|
||||
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: "./*"},
|
||||
}
|
||||
|
||||
// Should not error and should return all files
|
||||
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, gitignore 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 gitignore {
|
||||
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.IsGitignore())
|
||||
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.IsGitignore())
|
||||
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.IsGitignore())
|
||||
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.IsGitignore())
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user