236 lines
4.6 KiB
Go
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()
|
|
}
|