package ignore import ( "bufio" "os" "path/filepath" "strings" ) /* Ignore file parser for .larcignore and .gitignore. * Supports basic glob patterns like: * - *.log * - /build/ * - node_modules/ * - !important.log (negation) */ // Matcher checks if paths should be ignored type Matcher struct { patterns []pattern } type pattern struct { pattern string negation bool dirOnly bool } // New creates a new ignore matcher for the given repository root func New(root string) *Matcher { m := &Matcher{} /* load .gitignore first */ m.loadFile(filepath.Join(root, ".gitignore")) /* load .larcignore (overrides .gitignore) */ m.loadFile(filepath.Join(root, ".larcignore")) return m } func (m *Matcher) loadFile(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) /* skip empty lines and comments */ if line == "" || strings.HasPrefix(line, "#") { continue } p := pattern{pattern: line} /* check for negation */ if strings.HasPrefix(line, "!") { p.negation = true p.pattern = line[1:] } /* check for directory-only pattern */ if strings.HasSuffix(p.pattern, "/") { p.dirOnly = true p.pattern = strings.TrimSuffix(p.pattern, "/") } /* remove leading slash (makes it relative to root) */ p.pattern = strings.TrimPrefix(p.pattern, "/") m.patterns = append(m.patterns, p) } } // Match checks if a path should be ignored func (m *Matcher) Match(path string, isDir bool) bool { /* always ignore .larc directory */ if path == ".larc" || strings.HasPrefix(path, ".larc/") { return true } ignored := false for _, p := range m.patterns { /* skip dir-only patterns for files */ if p.dirOnly && !isDir { continue } if m.matchPattern(p.pattern, path) { ignored = !p.negation } } return ignored } func (m *Matcher) matchPattern(pattern, path string) bool { /* exact match */ if pattern == path { return true } /* check if pattern matches any component */ if !strings.Contains(pattern, "/") { /* pattern like "*.log" or "node_modules" */ name := filepath.Base(path) /* try glob match on filename */ if matched, _ := filepath.Match(pattern, name); matched { return true } /* also check if any path component matches */ parts := strings.Split(path, "/") for _, part := range parts { if matched, _ := filepath.Match(pattern, part); matched { return true } } return false } /* pattern with path separator */ /* try glob match on full path */ if matched, _ := filepath.Match(pattern, path); matched { return true } /* try matching as prefix */ if strings.HasPrefix(path, pattern+"/") { return true } /* try double-star pattern */ if strings.Contains(pattern, "**") { return m.matchDoubleStar(pattern, path) } return false } func (m *Matcher) matchDoubleStar(pattern, path string) bool { /* split pattern by ** */ parts := strings.Split(pattern, "**") if len(parts) != 2 { return false } prefix := parts[0] suffix := strings.TrimPrefix(parts[1], "/") /* check prefix */ if prefix != "" && !strings.HasPrefix(path, prefix) { return false } /* check suffix */ if suffix == "" { return true } /* find suffix in remaining path */ remaining := strings.TrimPrefix(path, prefix) pathParts := strings.Split(remaining, "/") for i := range pathParts { subpath := strings.Join(pathParts[i:], "/") if matched, _ := filepath.Match(suffix, subpath); matched { return true } /* also try matching just the filename part */ if matched, _ := filepath.Match(suffix, pathParts[len(pathParts)-1]); matched { return true } } return false } // ShouldIgnore is a convenience function func ShouldIgnore(root, path string, isDir bool) bool { m := New(root) return m.Match(path, isDir) }