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}6768pub fn parse(text string) Ignore {69 mut patterns := []Pattern{}70 for raw in text.split_into_lines() {71 line := raw.trim_space()72 if line == '' || line.starts_with('#') {73 continue74 }75 mut body := line76 mut dir_only := false77 if body.ends_with('/') {78 dir_only = true79 body = body.trim_right('/')80 }81 mut anchored := false82 if body.starts_with('/') {83 anchored = true84 body = body.trim_left('/')85 }86 if body == '' {87 continue88 }89 patterns << Pattern{90 segments: body.split('/')91 dir_only: dir_only92 anchored: anchored93 }94 }95 return Ignore{96 patterns: patterns97 }98}99100101102103104105106pub fn hard_ignored(rel string) bool {107 if rel == '.koh' || rel == '.koh-wal' || rel == '.koh-shm' {108 return true109 }110 if rel == '.koh-v2' || rel.starts_with('.koh-v2/') {111 return true112 }113 if rel == '.git' || rel.starts_with('.git/') {114 return true115 }116 if rel == 'LOG.md' {117 return true118 }119 return false120}121122123124125126pub fn (ig Ignore) matches(rel string, is_dir bool) bool {127 if hard_ignored(rel) {128 return true129 }130 parts := rel.split('/')131 for pat in ig.patterns {132 if ig.matches_pattern(parts, is_dir, pat) {133 return true134 }135 }136 return false137}138139fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {140 if pat.dir_only {141 142 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {143 return true144 }145 if !is_dir {146 return false147 }148 }149 150 for start in 0 .. parts.len {151 if pat.anchored && start != 0 {152 break153 }154 if match_segments(parts, start, pat.segments, 0, false) {155 return true156 }157 }158 return false159}160161162163fn match_any_prefix(parts []string, pat []string, anchored bool) bool {164 for start in 0 .. parts.len {165 if anchored && start != 0 {166 break167 }168 for end in start + 1 .. parts.len + 1 {169 if match_segments(parts[start..end], 0, pat, 0, false) {170 return true171 }172 }173 }174 return false175}176177178179180fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {181 if gi == pat.len {182 return allow_trailing || pi == path.len183 }184 seg := pat[gi]185 if seg == '**' {186 for skip in pi .. path.len + 1 {187 if match_segments(path, skip, pat, gi + 1, allow_trailing) {188 return true189 }190 }191 return false192 }193 if pi >= path.len {194 return false195 }196 if !match_segment(path[pi], seg) {197 return false198 }199 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)200}201202203fn match_segment(name string, pat string) bool {204 mut n := 0205 mut p := 0206 mut star := -1207 mut mark := 0208 for n < name.len {209 if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {210 n++211 p++212 } else if p < pat.len && pat[p] == `*` {213 star = p214 mark = n215 p++216 } else if star >= 0 {217 p = star + 1218 mark++219 n = mark220 } else {221 return false222 }223 }224 for p < pat.len && pat[p] == `*` {225 p++226 }227 return p == pat.len228}