ignore.v

5.3 KB · raw

1//! Ignore rules.2//!3//! `.kohignore` lives at the repo root and is itself snapshotted, so the rules4//! travel between machines with the history. Patterns are matched against5//! repo-relative paths: `*` matches inside one path segment, `**` spans6//! segments, a trailing `/` restricts a pattern to directories, and a leading7//! `/` anchors it to the repo root.89module snapshot1011import os1213// The seed written by `koh init`, carried over from Koh 2's default list.14pub 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}5859// load reads `.kohignore`; a repo without one simply has no extra rules.60pub 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}99100// hard_ignored covers the files Koh writes into a repo as part of working101// there. They are never snapshotted whatever `.kohignore` says.102//103// LOG.md is rewritten after every save from the history itself. Tracking it104// would make the tree dirty the moment a save lands, so `status` could never105// report a clean tree and every diff would carry the log's own churn.106pub 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}121122// matches reports whether a repo-relative path is ignored. `is_dir` matters123// because a pattern ending in `/` names a directory: it covers the directory124// itself and everything below it, but not a file that happens to share the125// name.126pub 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		// Anything inside an ignored directory is ignored too.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	// Otherwise the pattern has to match the whole path.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}160161// match_any_prefix asks whether the pattern matches a leading run of segments162// starting at any point in the path.163fn 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}176177// match_segments consumes `pat` from `gi` over `path` from `pi`.178// `allow_trailing` lets the path keep segments the pattern does not name,179// which is what makes a directory pattern cover everything beneath it.180fn 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}201202// match_segment is shell-style matching within one path segment.203fn 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}