larc r8

191 lines ยท 3.8 KB Raw
1 package ignore
2
3 import (
4 "bufio"
5 "os"
6 "path/filepath"
7 "strings"
8 )
9
10 /* Ignore file parser for .larcignore and .gitignore.
11 * Supports basic glob patterns like:
12 * - *.log
13 * - /build/
14 * - node_modules/
15 * - !important.log (negation)
16 */
17
18 // Matcher checks if paths should be ignored
19 type Matcher struct {
20 patterns []pattern
21 }
22
23 type pattern struct {
24 pattern string
25 negation bool
26 dirOnly bool
27 }
28
29 // New creates a new ignore matcher for the given repository root
30 func New(root string) *Matcher {
31 m := &Matcher{}
32
33 /* load .gitignore first */
34 m.loadFile(filepath.Join(root, ".gitignore"))
35
36 /* load .larcignore (overrides .gitignore) */
37 m.loadFile(filepath.Join(root, ".larcignore"))
38
39 return m
40 }
41
42 func (m *Matcher) loadFile(path string) {
43 f, err := os.Open(path)
44 if err != nil {
45 return
46 }
47 defer f.Close()
48
49 scanner := bufio.NewScanner(f)
50 for scanner.Scan() {
51 line := strings.TrimSpace(scanner.Text())
52
53 /* skip empty lines and comments */
54 if line == "" || strings.HasPrefix(line, "#") {
55 continue
56 }
57
58 p := pattern{pattern: line}
59
60 /* check for negation */
61 if strings.HasPrefix(line, "!") {
62 p.negation = true
63 p.pattern = line[1:]
64 }
65
66 /* check for directory-only pattern */
67 if strings.HasSuffix(p.pattern, "/") {
68 p.dirOnly = true
69 p.pattern = strings.TrimSuffix(p.pattern, "/")
70 }
71
72 /* remove leading slash (makes it relative to root) */
73 p.pattern = strings.TrimPrefix(p.pattern, "/")
74
75 m.patterns = append(m.patterns, p)
76 }
77 }
78
79 // Match checks if a path should be ignored
80 func (m *Matcher) Match(path string, isDir bool) bool {
81 /* always ignore .larc directory */
82 if path == ".larc" || strings.HasPrefix(path, ".larc/") {
83 return true
84 }
85
86 ignored := false
87
88 for _, p := range m.patterns {
89 /* skip dir-only patterns for files */
90 if p.dirOnly && !isDir {
91 continue
92 }
93
94 if m.matchPattern(p.pattern, path) {
95 ignored = !p.negation
96 }
97 }
98
99 return ignored
100 }
101
102 func (m *Matcher) matchPattern(pattern, path string) bool {
103 /* exact match */
104 if pattern == path {
105 return true
106 }
107
108 /* check if pattern matches any component */
109 if !strings.Contains(pattern, "/") {
110 /* pattern like "*.log" or "node_modules" */
111 name := filepath.Base(path)
112
113 /* try glob match on filename */
114 if matched, _ := filepath.Match(pattern, name); matched {
115 return true
116 }
117
118 /* also check if any path component matches */
119 parts := strings.Split(path, "/")
120 for _, part := range parts {
121 if matched, _ := filepath.Match(pattern, part); matched {
122 return true
123 }
124 }
125
126 return false
127 }
128
129 /* pattern with path separator */
130 /* try glob match on full path */
131 if matched, _ := filepath.Match(pattern, path); matched {
132 return true
133 }
134
135 /* try matching as prefix */
136 if strings.HasPrefix(path, pattern+"/") {
137 return true
138 }
139
140 /* try double-star pattern */
141 if strings.Contains(pattern, "**") {
142 return m.matchDoubleStar(pattern, path)
143 }
144
145 return false
146 }
147
148 func (m *Matcher) matchDoubleStar(pattern, path string) bool {
149 /* split pattern by ** */
150 parts := strings.Split(pattern, "**")
151 if len(parts) != 2 {
152 return false
153 }
154
155 prefix := parts[0]
156 suffix := strings.TrimPrefix(parts[1], "/")
157
158 /* check prefix */
159 if prefix != "" && !strings.HasPrefix(path, prefix) {
160 return false
161 }
162
163 /* check suffix */
164 if suffix == "" {
165 return true
166 }
167
168 /* find suffix in remaining path */
169 remaining := strings.TrimPrefix(path, prefix)
170 pathParts := strings.Split(remaining, "/")
171
172 for i := range pathParts {
173 subpath := strings.Join(pathParts[i:], "/")
174 if matched, _ := filepath.Match(suffix, subpath); matched {
175 return true
176 }
177 /* also try matching just the filename part */
178 if matched, _ := filepath.Match(suffix, pathParts[len(pathParts)-1]); matched {
179 return true
180 }
181 }
182
183 return false
184 }
185
186 // ShouldIgnore is a convenience function
187 func ShouldIgnore(root, path string, isDir bool) bool {
188 m := New(root)
189 return m.Match(path, isDir)
190 }
191