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}99100101102103104105106107108pub fn hard_ignored(rel string) bool {109 if rel == 'LOG.md' {110 return true111 }112 return rel.split('/').any(it in hard_ignored_names)113}114115const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']116117118119120121pub fn (ig Ignore) matches(rel string, is_dir bool) bool {122 if hard_ignored(rel) {123 return true124 }125 parts := rel.split('/')126 for pat in ig.patterns {127 if ig.matches_pattern(parts, is_dir, pat) {128 return true129 }130 }131 return false132}133134fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {135 if pat.dir_only {136 137 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {138 return true139 }140 if !is_dir {141 return false142 }143 }144 145 for start in 0 .. parts.len {146 if pat.anchored && start != 0 {147 break148 }149 if match_segments(parts, start, pat.segments, 0, false) {150 return true151 }152 }153 return false154}155156157158fn match_any_prefix(parts []string, pat []string, anchored bool) bool {159 for start in 0 .. parts.len {160 if anchored && start != 0 {161 break162 }163 for end in start + 1 .. parts.len + 1 {164 if match_segments(parts[start..end], 0, pat, 0, false) {165 return true166 }167 }168 }169 return false170}171172173174175fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {176 if gi == pat.len {177 return allow_trailing || pi == path.len178 }179 seg := pat[gi]180 if seg == '**' {181 for skip in pi .. path.len + 1 {182 if match_segments(path, skip, pat, gi + 1, allow_trailing) {183 return true184 }185 }186 return false187 }188 if pi >= path.len {189 return false190 }191 if !match_segment(path[pi], seg) {192 return false193 }194 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)195}196197198fn match_segment(name string, pat string) bool {199 mut n := 0200 mut p := 0201 mut star := -1202 mut mark := 0203 for n < name.len {204 if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {205 n++206 p++207 } else if p < pat.len && pat[p] == `*` {208 star = p209 mark = n210 p++211 } else if star >= 0 {212 p = star + 1213 mark++214 n = mark215 } else {216 return false217 }218 }219 for p < pat.len && pat[p] == `*` {220 p++221 }222 return p == pat.len223}