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_Store37Icon\\r38Thumbs.db3940# Logs41*.log4243# Koh artifacts44*.bundle45*.koh.offer46'4748pub const ignore_file = '.kohignore'4950struct Pattern {51 segments []string52 dir_only bool53 anchored bool54}5556pub struct Ignore {57 patterns []Pattern58}596061pub fn load(root string) Ignore {62 path := os.join_path(root, ignore_file)63 if !os.is_file(path) {64 return Ignore{}65 }66 return parse(os.read_file(path) or { return Ignore{} })67}68697071fn (ig Ignore) banning(paths []string) Ignore {72 mut patterns := ig.patterns.clone()73 for path in paths {74 patterns << Pattern{75 segments: literal(path).split('/')76 anchored: true77 }78 }79 return Ignore{80 patterns: patterns81 }82}83848586pub fn add_ignore(root string, path string) !bool {87 file := os.join_path(root, ignore_file)88 text := if os.is_file(file) { os.read_file(file)! } else { '' }89 rule := '/${literal(path)}'90 if text.split_into_lines().any(it.trim_space() == rule) {91 return false92 }93 separator := if text == '' || text.ends_with('\n') { '' } else { '\n' }94 os.write_file(file, '${text}${separator}${rule}\n')!95 return true96}979899fn literal(path string) string {100 return path.replace('\\', '\\\\').replace('*', '\\*').replace('?', '\\?')101}102103pub fn parse(text string) Ignore {104 mut patterns := []Pattern{}105 for raw in text.split_into_lines() {106 line := raw.trim_space()107 if line == '' || line.starts_with('#') {108 continue109 }110 mut body := line111 mut dir_only := false112 if body.ends_with('/') {113 dir_only = true114 body = body.trim_right('/')115 }116 mut anchored := false117 if body.starts_with('/') {118 anchored = true119 body = body.trim_left('/')120 }121 if body == '' {122 continue123 }124 patterns << Pattern{125 segments: body.split('/')126 dir_only: dir_only127 anchored: anchored128 }129 }130 return Ignore{131 patterns: patterns132 }133}134135136137138139140141142143pub fn hard_ignored(rel string) bool {144 if rel == 'Icon\r' {145 return true146 }147 if rel == 'LOG.md' {148 return true149 }150 return rel.split('/').any(it in hard_ignored_names)151}152153const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']154155156157158159pub fn (ig Ignore) matches(rel string, is_dir bool) bool {160 if hard_ignored(rel) {161 return true162 }163 parts := rel.split('/')164 for pat in ig.patterns {165 if ig.matches_pattern(parts, is_dir, pat) {166 return true167 }168 }169 return false170}171172fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {173 if pat.dir_only {174 175 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {176 return true177 }178 if !is_dir {179 return false180 }181 }182 183 for start in 0 .. parts.len {184 if pat.anchored && start != 0 {185 break186 }187 if match_segments(parts, start, pat.segments, 0, false) {188 return true189 }190 }191 return false192}193194195196fn match_any_prefix(parts []string, pat []string, anchored bool) bool {197 for start in 0 .. parts.len {198 if anchored && start != 0 {199 break200 }201 for end in start + 1 .. parts.len + 1 {202 if match_segments(parts[start..end], 0, pat, 0, false) {203 return true204 }205 }206 }207 return false208}209210211212213fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {214 if gi == pat.len {215 return allow_trailing || pi == path.len216 }217 seg := pat[gi]218 if seg == '**' {219 for skip in pi .. path.len + 1 {220 if match_segments(path, skip, pat, gi + 1, allow_trailing) {221 return true222 }223 }224 return false225 }226 if pi >= path.len {227 return false228 }229 if !match_segment(path[pi], seg) {230 return false231 }232 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)233}234235236fn match_segment(name string, pat string) bool {237 mut n := 0238 mut p := 0239 mut star := -1240 mut mark := 0241 for n < name.len {242 escaped := p + 1 < pat.len && pat[p] == `\\`243 if escaped && pat[p + 1] == name[n] {244 n++245 p += 2246 } else if !escaped && p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {247 n++248 p++249 } else if !escaped && p < pat.len && pat[p] == `*` {250 star = p251 mark = n252 p++253 } else if star >= 0 {254 p = star + 1255 mark++256 n = mark257 } else {258 return false259 }260 }261 for p < pat.len && pat[p] == `*` {262 p++263 }264 return p == pat.len265}