buildr/internal/ignore/ignorer.go
Peter 1261932bdc
All checks were successful
continuous-integration/drone/push Build is passing
refactor: apply golangci-lint findings
2023-06-22 19:16:00 +02:00

236 lines
4.6 KiB
Go

package ignore
import (
"bufio"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
const FileName = ".buildrignore"
var (
_ fs.StatFS = (*Ignorer)(nil)
_ fs.ReadDirFS = (*Ignorer)(nil)
)
func MustNewIgnorer(dir string, additionalPatterns ...string) *Ignorer {
ig, err := NewIgnorer(dir, additionalPatterns...)
if err != nil {
panic(err)
}
return ig
}
func NewIgnorer(dir string, additionalPatterns ...string) (*Ignorer, error) {
ig := &Ignorer{
root: dir,
fs: os.DirFS(dir),
}
if err := ig.AddPatterns(additionalPatterns...); err != nil {
return nil, err
}
ignoreFile, err := os.Open(filepath.Join(dir, FileName))
if err != nil {
if os.IsNotExist(err) {
return ig, nil
}
return nil, err
}
defer func() {
_ = ignoreFile.Close()
}()
if patterns, err := readGitIgnore(ignoreFile); err != nil {
return nil, err
} else if err := ig.AddPatterns(patterns...); err != nil {
return nil, err
}
return ig, nil
}
type Ignorer struct {
fs fs.FS
parent *Ignorer
root string
patterns []ignorePattern
}
func (ig *Ignorer) SetParent(parent *Ignorer) {
ig.parent = parent
}
func (ig *Ignorer) AddPatterns(patterns ...string) error {
for i := range patterns {
if parsed, err := parsePattern(patterns[i]); err != nil {
return err
} else {
ig.patterns = append(ig.patterns, *parsed)
}
}
return nil
}
func (ig *Ignorer) Ignore(path string) (ignore bool) {
if ig.parent != nil {
var match bool
if match, ignore = ig.parent.computeIgnore(path); match {
return ignore
}
}
_, ignore = ig.computeIgnore(path)
return ignore
}
func (ig *Ignorer) ReadDir(name string) (result []fs.DirEntry, err error) {
if ig.Ignore(name) {
return nil, nil
}
var isPartOfFS bool
if name, isPartOfFS, err = ig.normalize(name); err != nil {
return nil, err
} else if !isPartOfFS {
return os.ReadDir(name)
}
entries, err := fs.ReadDir(ig.fs, name)
if err != nil {
return nil, err
}
result = make([]fs.DirEntry, 0, len(entries))
for i := range entries {
if entry := entries[i]; ig.Ignore(filepath.Join(ig.root, name, entry.Name())) {
continue
} else {
result = append(result, entry)
}
}
return result, nil
}
func (ig *Ignorer) Open(name string) (f fs.File, err error) {
var isPartOfFS bool
if name, isPartOfFS, err = ig.normalize(name); err != nil {
return nil, err
} else if !isPartOfFS {
return os.Open(name)
}
return ig.fs.Open(name)
}
func (ig *Ignorer) Stat(name string) (info fs.FileInfo, err error) {
var isPartOfFS bool
if name, isPartOfFS, err = ig.normalize(name); err != nil {
return nil, err
} else if !isPartOfFS {
return os.Stat(name)
}
info, err = fs.Stat(ig.fs, name)
if err != nil {
return nil, fmt.Errorf("failed to stat %s: %w", name, err)
}
return info, nil
}
func (ig *Ignorer) WalkDir(root string, walkFunc fs.WalkDirFunc) (err error) {
var isPartOfFS bool
if root, isPartOfFS, err = ig.normalize(root); err != nil {
return err
} else if !isPartOfFS {
return fmt.Errorf("root %s is not a sub-directory of the files to be ignored", root)
}
return fs.WalkDir(ig, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
absolutePath := filepath.Join(ig.root, path)
if path != root {
localIgnoreFile := filepath.Join(path, FileName)
if _, err := ig.Stat(localIgnoreFile); err == nil {
child, err := ig.child(path)
if err != nil {
return err
}
if err := child.WalkDir(absolutePath, walkFunc); err != nil {
return err
}
return fs.SkipDir
}
}
return walkFunc(absolutePath, d, nil)
})
}
func (ig *Ignorer) child(path string) (*Ignorer, error) {
child, err := NewIgnorer(path)
if err != nil {
return nil, err
}
child.parent = ig
return child, nil
}
func (ig *Ignorer) normalize(name string) (normalized string, isPartOfFs bool, err error) {
if !filepath.IsAbs(name) {
return name, true, nil
}
if !strings.HasPrefix(name, ig.root) {
return name, false, nil
}
normalized, err = filepath.Rel(ig.root, name)
if err != nil {
return "", false, fmt.Errorf("failed to normalize path %s: %w", name, err)
}
return normalized, true, nil
}
func (ig *Ignorer) computeIgnore(path string) (match, ignore bool) {
for i := range ig.patterns {
if match, ignore = ig.patterns[i].ignore(path); match {
return
}
}
return false, false
}
func readGitIgnore(content io.Reader) (patterns []string, err error) {
scanner := bufio.NewScanner(content)
for scanner.Scan() {
pattern := strings.TrimSpace(scanner.Text())
if len(pattern) == 0 || pattern[0] == '#' {
continue
}
patterns = append(patterns, pattern)
}
return patterns, scanner.Err()
}