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}121122pub fn (ig Ignore) matches(rel string) bool {123 if hard_ignored(rel) {124 return true125 }126 parts := rel.split('/')127 for pat in ig.patterns {128 if ig.matches_pattern(parts, pat) {129 return true130 }131 }132 return false133}134135fn (ig Ignore) matches_pattern(parts []string, pat Pattern) bool {136 if pat.anchored {137 138 if pat.dir_only {139 return parts.len >= 2 && match_segments(parts, 0, pat.segments, 0, true)140 }141 return match_segments(parts, 0, pat.segments, 0, false)142 }143 if pat.dir_only {144 for start in 0 .. parts.len {145 if parts.len - start >= 2 && match_segments(parts, start, pat.segments, 0, true) {146 return true147 }148 }149 return false150 }151 for start in 0 .. parts.len {152 if match_segments(parts, start, pat.segments, 0, false) {153 return true154 }155 }156 return false157}158159160161162fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {163 if gi == pat.len {164 return allow_trailing || pi == path.len165 }166 seg := pat[gi]167 if seg == '**' {168 for skip in pi .. path.len + 1 {169 if match_segments(path, skip, pat, gi + 1, allow_trailing) {170 return true171 }172 }173 return false174 }175 if pi >= path.len {176 return false177 }178 if !match_segment(path[pi], seg) {179 return false180 }181 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)182}183184185fn match_segment(name string, pat string) bool {186 mut n := 0187 mut p := 0188 mut star := -1189 mut mark := 0190 for n < name.len {191 if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {192 n++193 p++194 } else if p < pat.len && pat[p] == `*` {195 star = p196 mark = n197 p++198 } else if star >= 0 {199 p = star + 1200 mark++201 n = mark202 } else {203 return false204 }205 }206 for p < pat.len && pat[p] == `*` {207 p++208 }209 return p == pat.len210}