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 global_ignore_path() string {62 return os.join_path(os.home_dir(), ignore_file)63}646566pub fn load(root string) Ignore {67 local := os.read_file(os.join_path(root, ignore_file)) or { '' }68 global := os.read_file(global_ignore_path()) or { '' }69 return parse(global + '\n' + local)70}717273pub fn matching_rules(text string, rel string, is_dir bool) []string {74 mut rules := []string{}75 for raw in text.split_into_lines() {76 rule := raw.trim_space()77 if rule == '' || rule.starts_with('#') {78 continue79 }80 ig := parse(rule)81 mut matched := ig.matches(rel, is_dir)82 parts := rel.split('/')83 for end in 1 .. parts.len {84 matched = matched || ig.matches(parts[..end].join('/'), true)85 }86 if matched && rule !in rules {87 rules << rule88 }89 }90 return rules91}929394pub fn ignore_rule(path string, is_dir bool, global bool) string {95 prefix := if global { '' } else { '/' }96 suffix := if is_dir { '/' } else { '' }97 return prefix + literal(path) + suffix98}99100101102fn (ig Ignore) banning(paths []string) Ignore {103 mut patterns := ig.patterns.clone()104 for path in paths {105 patterns << Pattern{106 segments: literal(path).split('/')107 anchored: true108 }109 }110 return Ignore{111 patterns: patterns112 }113}114115116117pub fn add_ignore(root string, path string) !bool {118 file := os.join_path(root, ignore_file)119 text := if os.is_file(file) { os.read_file(file)! } else { '' }120 rule := '/${literal(path)}'121 if text.split_into_lines().any(it.trim_space() == rule) {122 return false123 }124 separator := if text == '' || text.ends_with('\n') { '' } else { '\n' }125 os.write_file(file, '${text}${separator}${rule}\n')!126 return true127}128129130fn literal(path string) string {131 return path.replace('\\', '\\\\').replace('*', '\\*').replace('?', '\\?').replace('#', '\\#')132}133134pub fn parse(text string) Ignore {135 mut patterns := []Pattern{}136 for raw in text.split_into_lines() {137 line := raw.trim_space()138 if line == '' || line.starts_with('#') {139 continue140 }141 mut body := line142 mut dir_only := false143 if body.ends_with('/') {144 dir_only = true145 body = body.trim_right('/')146 }147 mut anchored := false148 if body.starts_with('/') {149 anchored = true150 body = body.trim_left('/')151 }152 if body == '' {153 continue154 }155 patterns << Pattern{156 segments: body.split('/')157 dir_only: dir_only158 anchored: anchored159 }160 }161 return Ignore{162 patterns: patterns163 }164}165166167168169170171172173174pub fn hard_ignored(rel string) bool {175 if rel == 'Icon\r' {176 return true177 }178 if rel == 'LOG.md' {179 return true180 }181 return rel.split('/').any(it in hard_ignored_names)182}183184const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']185186187188189190pub fn (ig Ignore) matches(rel string, is_dir bool) bool {191 if hard_ignored(rel) {192 return true193 }194 parts := rel.split('/')195 for pat in ig.patterns {196 if ig.matches_pattern(parts, is_dir, pat) {197 return true198 }199 }200 return false201}202203fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {204 if pat.dir_only {205 206 if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {207 return true208 }209 if !is_dir {210 return false211 }212 }213 214 for start in 0 .. parts.len {215 if pat.anchored && start != 0 {216 break217 }218 if match_segments(parts, start, pat.segments, 0, false) {219 return true220 }221 }222 return false223}224225226227fn match_any_prefix(parts []string, pat []string, anchored bool) bool {228 for start in 0 .. parts.len {229 if anchored && start != 0 {230 break231 }232 for end in start + 1 .. parts.len + 1 {233 if match_segments(parts[start..end], 0, pat, 0, false) {234 return true235 }236 }237 }238 return false239}240241242243244fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {245 if gi == pat.len {246 return allow_trailing || pi == path.len247 }248 seg := pat[gi]249 if seg == '**' {250 for skip in pi .. path.len + 1 {251 if match_segments(path, skip, pat, gi + 1, allow_trailing) {252 return true253 }254 }255 return false256 }257 if pi >= path.len {258 return false259 }260 if !match_segment(path[pi], seg) {261 return false262 }263 return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)264}265266267fn match_segment(name string, pat string) bool {268 mut n := 0269 mut p := 0270 mut star := -1271 mut mark := 0272 for n < name.len {273 escaped := p + 1 < pat.len && pat[p] == `\\`274 if escaped && pat[p + 1] == name[n] {275 n++276 p += 2277 } else if !escaped && p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {278 n++279 p++280 } else if !escaped && p < pat.len && pat[p] == `*` {281 star = p282 mark = n283 p++284 } else if star >= 0 {285 p = star + 1286 mark++287 n = mark288 } else {289 return false290 }291 }292 for p < pat.len && pat[p] == `*` {293 p++294 }295 return p == pat.len296}