-
-
Notifications
You must be signed in to change notification settings - Fork 841
feat: add use_gitignore option to exclude ignored files from sources/generates #2773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fe542d5
ae3627c
13ef1b2
7cea8e3
7705f92
de99487
70fe293
7d92de8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package fingerprint | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/go-task/task/v3/internal/gitignore" | ||
| ) | ||
|
|
||
| type gitignoreRule struct { | ||
| dir string | ||
| matcher gitignore.Matcher | ||
| } | ||
|
|
||
| // loadGitignoreRules walks up from dir collecting .gitignore files. | ||
| // Stops at the first .git (file or directory) found. | ||
| // Returns nil if no .git is found (not in a git repo). | ||
| func loadGitignoreRules(dir string) []gitignoreRule { | ||
| dir, _ = filepath.Abs(dir) | ||
|
|
||
| var rules []gitignoreRule | ||
| foundGit := false | ||
| current := dir | ||
|
|
||
| for { | ||
| lines := readGitignoreLines(filepath.Join(current, ".gitignore")) | ||
| if len(lines) > 0 { | ||
| patterns := make([]gitignore.Pattern, 0, len(lines)) | ||
| for _, line := range lines { | ||
| patterns = append(patterns, gitignore.ParsePattern(line, nil)) | ||
| } | ||
| rules = append(rules, gitignoreRule{ | ||
| dir: current, | ||
| matcher: gitignore.NewMatcher(patterns), | ||
| }) | ||
| } | ||
| if _, err := os.Stat(filepath.Join(current, ".git")); err == nil { | ||
| foundGit = true | ||
| break | ||
| } | ||
| parent := filepath.Dir(current) | ||
| if parent == current { | ||
| break | ||
| } | ||
| current = parent | ||
| } | ||
|
|
||
| if !foundGit { | ||
| return nil | ||
| } | ||
|
|
||
| return rules | ||
| } | ||
|
|
||
| func readGitignoreLines(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) | ||
| } | ||
| } | ||
| if err := scanner.Err(); err != nil { | ||
| return nil | ||
| } | ||
| return lines | ||
| } | ||
|
|
||
| // filterGitignored removes entries from the file map that match gitignore rules. | ||
| func filterGitignored(files map[string]bool, dir string) map[string]bool { | ||
| rules := loadGitignoreRules(dir) | ||
| if len(rules) == 0 { | ||
| return files | ||
| } | ||
|
|
||
| for path := range files { | ||
| for _, rule := range rules { | ||
| relPath, err := filepath.Rel(rule.dir, path) | ||
| if err != nil || strings.HasPrefix(relPath, "..") { | ||
| continue | ||
| } | ||
| // Sources are files, not directories; pass isDir=false. Per the | ||
| // gitignore spec this still matches files under an ignored dir | ||
| // (e.g. "build/" matches build/out.txt). | ||
| if rule.matcher.Match(strings.Split(filepath.ToSlash(relPath), "/"), false) { | ||
| files[path] = false | ||
| break | ||
| } | ||
| } | ||
|
Comment on lines
+85
to
+98
|
||
| } | ||
|
|
||
| return files | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package fingerprint | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "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 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 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filterGitignoredloads.gitignorerules only based on the task/basedir(vialoadGitignoreRules(dir)), so glob matches that live in subdirectories won't respect.gitignorefiles inside those subdirectories (e.g. sources like./**/*). To match Git behavior, rule discovery needs to be based on each matched file’s directory (and its parents up to the repo root), ideally with caching to avoid repeated filesystem walks.