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: literal(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 := '/${literal(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}969798fn literal(path string) string {99 return path.replace('\\', '\\\\').replace('*', '\\*').replace('?', '\\?')100}101102pub fn parse(text string) Ignore {103 mut patterns := []Pattern{}104 for raw in text.split_into_lines() {105 line := raw.trim_space()106 if line == '' || line.starts_with('#') {107 continue108 }109 mut body := line110 mut dir_only := false111 if body.ends_with('/') {112 dir_only = true113 body = body.trim_right('/')114 }115 mut anchored := false116 if body.starts_with('/') {117 anchored = true118 body = body.trim_left('/')119 }120 if body == '' {121 continue122 }123 patterns << Pattern{124 segments: body.split('/')125 dir_only: dir_only126 anchored: anchored127 }128 }129 return Ignore{130 patterns: patterns131 }132}133134135136137138139140141142pub fn hard_ignored(rel string) bool {143 if rel == 'LOG.md' {144 return true145 }146 return rel.split('/').any(it in hard_ignored_names)147}148149const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']150151152153154155pub fn (ig Ignore) matches(rel string, is_dir bool) bool {156 if hard_ignored(rel) {157 return true158 }159 parts := rel.split('/')160 for pat in ig.patterns {161 if ig.matches_pattern(parts, is_dir, pat) {162 return true163 }164 }165 return false166}167168fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {169 if pat.dir_only {170 171 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {172 return true173 }174 if !is_dir {175 return false176 }177 }178 179 for start in 0 .. parts.len {180 if pat.anchored && start != 0 {181 break182 }183 if match_segments(parts, start, pat.segments, 0, false) {184 return true185 }186 }187 return false188}189190191192fn match_any_prefix(parts []string, pat []string, anchored bool) bool {193 for start in 0 .. parts.len {194 if anchored && start != 0 {195 break196 }197 for end in start + 1 .. parts.len + 1 {198 if match_segments(parts[start..end], 0, pat, 0, false) {199 return true200 }201 }202 }203 return false204}205206207208209fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {210 if gi == pat.len {211 return allow_trailing || pi == path.len212 }213 seg := pat[gi]214 if seg == '**' {215 for skip in pi .. path.len + 1 {216 if match_segments(path, skip, pat, gi + 1, allow_trailing) {217 return true218 }219 }220 return false221 }222 if pi >= path.len {223 return false224 }225 if !match_segment(path[pi], seg) {226 return false227 }228 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)229}230231232fn match_segment(name string, pat string) bool {233 mut n := 0234 mut p := 0235 mut star := -1236 mut mark := 0237 for n < name.len {238 escaped := p + 1 < pat.len && pat[p] == `\\`239 if escaped && pat[p + 1] == name[n] {240 n++241 p += 2242 } else if !escaped && p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {243 n++244 p++245 } else if !escaped && p < pat.len && pat[p] == `*` {246 star = p247 mark = n248 p++249 } else if star >= 0 {250 p = star + 1251 mark++252 n = mark253 } else {254 return false255 }256 }257 for p < pat.len && pat[p] == `*` {258 p++259 }260 return p == pat.len261}