123456789module snapshot1011import os121314pub const default_ignore = '# koh ignore rules (.kohignore)15# Generated by koh init. Edit to fit the project.1617# Patterns are matched against repo-relative paths. Suffix `/` restricts18# to directories. Leading `/` anchors to the repo root. A pattern like19# `*.log` matches any file with that extension anywhere in the tree.20#21# koh already ignores .koh, .koh-wal, .koh-shm, .koh-v2/ and .git/ automatically.2223# Build output24zig-out/25.zig-cache/26zig-cache/27target/28build/29dist/30out/3132# Dependencies33node_modules/3435# OS noise36.DS_Store37Thumbs.db3839# Logs40*.log4142# Koh artifacts43*.bundle44*.koh.offer45'4647pub const ignore_file = '.kohignore'4849struct Pattern {50 segments []string51 dir_only bool52 anchored bool53}5455pub struct Ignore {56 patterns []Pattern57}585960pub fn load(root string) Ignore {61 path := os.join_path(root, ignore_file)62 if !os.is_file(path) {63 return Ignore{}64 }65 return parse(os.read_file(path) or { return Ignore{} })66}67686970fn (ig Ignore) banning(paths []string) Ignore {71 mut patterns := ig.patterns.clone()72 for path in paths {73 patterns << Pattern{74 segments: path.split('/')75 anchored: true76 }77 }78 return Ignore{79 patterns: patterns80 }81}82838485pub fn add_ignore(root string, path string) !bool {86 file := os.join_path(root, ignore_file)87 text := if os.is_file(file) { os.read_file(file)! } else { '' }88 rule := '/${path}'89 if text.split_into_lines().any(it.trim_space() == rule) {90 return false91 }92 separator := if text == '' || text.ends_with('\n') { '' } else { '\n' }93 os.write_file(file, '${text}${separator}${rule}\n')!94 return true95}9697pub fn parse(text string) Ignore {98 mut patterns := []Pattern{}99 for raw in text.split_into_lines() {100 line := raw.trim_space()101 if line == '' || line.starts_with('#') {102 continue103 }104 mut body := line105 mut dir_only := false106 if body.ends_with('/') {107 dir_only = true108 body = body.trim_right('/')109 }110 mut anchored := false111 if body.starts_with('/') {112 anchored = true113 body = body.trim_left('/')114 }115 if body == '' {116 continue117 }118 patterns << Pattern{119 segments: body.split('/')120 dir_only: dir_only121 anchored: anchored122 }123 }124 return Ignore{125 patterns: patterns126 }127}128129130131132133134135136137pub fn hard_ignored(rel string) bool {138 if rel == 'LOG.md' {139 return true140 }141 return rel.split('/').any(it in hard_ignored_names)142}143144const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']145146147148149150pub fn (ig Ignore) matches(rel string, is_dir bool) bool {151 if hard_ignored(rel) {152 return true153 }154 parts := rel.split('/')155 for pat in ig.patterns {156 if ig.matches_pattern(parts, is_dir, pat) {157 return true158 }159 }160 return false161}162163fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {164 if pat.dir_only {165 166 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {167 return true168 }169 if !is_dir {170 return false171 }172 }173 174 for start in 0 .. parts.len {175 if pat.anchored && start != 0 {176 break177 }178 if match_segments(parts, start, pat.segments, 0, false) {179 return true180 }181 }182 return false183}184185186187fn match_any_prefix(parts []string, pat []string, anchored bool) bool {188 for start in 0 .. parts.len {189 if anchored && start != 0 {190 break191 }192 for end in start + 1 .. parts.len + 1 {193 if match_segments(parts[start..end], 0, pat, 0, false) {194 return true195 }196 }197 }198 return false199}200201202203204fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {205 if gi == pat.len {206 return allow_trailing || pi == path.len207 }208 seg := pat[gi]209 if seg == '**' {210 for skip in pi .. path.len + 1 {211 if match_segments(path, skip, pat, gi + 1, allow_trailing) {212 return true213 }214 }215 return false216 }217 if pi >= path.len {218 return false219 }220 if !match_segment(path[pi], seg) {221 return false222 }223 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)224}225226227fn match_segment(name string, pat string) bool {228 mut n := 0229 mut p := 0230 mut star := -1231 mut mark := 0232 for n < name.len {233 if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {234 n++235 p++236 } else if p < pat.len && pat[p] == `*` {237 star = p238 mark = n239 p++240 } else if star >= 0 {241 p = star + 1242 mark++243 n = mark244 } else {245 return false246 }247 }248 for p < pat.len && pat[p] == `*` {249 p++250 }251 return p == pat.len252}